loading
Generated 2022-06-09T12:14:13+01:00

All Files ( 90.8% covered at 189.21 hits/line )

350 files in total.
7584 relevant lines, 6886 lines covered and 698 lines missed. ( 90.8% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/components/allocation_record_card/view.rb 100.00 % 38 21 21 0 8.00
app/components/degree_preview_component/view.rb 86.67 % 36 15 13 2 1.07
app/components/degree_row_content_component/view.rb 94.44 % 40 18 17 1 10.44
app/components/filters/allocation_attributes/view.rb 100.00 % 12 7 7 0 4.71
app/components/filters/provider_attributes/view.rb 100.00 % 12 7 7 0 9.71
app/components/filters/user_attributes/view.rb 100.00 % 22 12 12 0 10.50
app/components/filters/view.rb 100.00 % 42 22 22 0 16.68
app/components/flash_banner/view.rb 100.00 % 18 9 9 0 335.33
app/components/gcse_preview_component/view.rb 100.00 % 7 3 3 0 1.00
app/components/gcse_requirements_component/view.rb 80.00 % 88 45 36 9 10.84
app/components/gcse_row_content_component/view.rb 100.00 % 6 2 2 0 4.00
app/components/header/view.rb 100.00 % 17 9 9 0 445.78
app/components/navigation_bar/view.rb 100.00 % 28 16 16 0 17.69
app/components/notification_banner/view.rb 100.00 % 74 36 36 0 40.67
app/components/page_title/view.rb 100.00 % 33 17 17 0 155.12
app/components/paginated_filter/view.rb 100.00 % 27 12 12 0 19.83
app/components/paginator/view.rb 100.00 % 80 36 36 0 15.19
app/components/persona/view.rb 100.00 % 20 8 8 0 3.00
app/components/phase_banner/view.rb 100.00 % 32 12 12 0 502.33
app/components/providers/provider_list/view.rb 100.00 % 20 11 11 0 8.18
app/components/tab_navigation/view.rb 100.00 % 12 6 6 0 28.33
app/components/title_bar/view.rb 100.00 % 16 8 8 0 3.75
app/controllers/api/public/v1/application_controller.rb 100.00 % 9 5 5 0 4.00
app/controllers/api/public/v1/courses_controller.rb 100.00 % 36 14 14 0 5.86
app/controllers/api/public/v1/provider_suggestions_controller.rb 100.00 % 33 13 13 0 2.15
app/controllers/api/public/v1/providers/courses/locations_controller.rb 100.00 % 44 21 21 0 3.29
app/controllers/api/public/v1/providers/courses_controller.rb 100.00 % 44 20 20 0 5.30
app/controllers/api/public/v1/providers/locations_controller.rb 100.00 % 33 16 16 0 2.81
app/controllers/api/public/v1/providers_controller.rb 100.00 % 72 33 33 0 9.88
app/controllers/api/public/v1/subject_areas_controller.rb 100.00 % 15 6 6 0 2.50
app/controllers/api/public/v1/subjects_controller.rb 100.00 % 27 13 13 0 1.92
app/controllers/api/system/application_controller.rb 100.00 % 16 7 7 0 1.29
app/controllers/api/v3/accredited_provider_training_providers_controller.rb 85.71 % 91 42 36 6 7.60
app/controllers/api/v3/application_controller.rb 88.24 % 67 34 30 4 21.85
app/controllers/api/v3/courses_controller.rb 100.00 % 69 28 28 0 16.75
app/controllers/api/v3/provider_suggestions_controller.rb 90.91 % 27 11 10 1 2.18
app/controllers/api/v3/providers_controller.rb 100.00 % 56 22 22 0 7.64
app/controllers/api/v3/subject_areas_controller.rb 100.00 % 9 5 5 0 1.40
app/controllers/api/v3/subjects_controller.rb 100.00 % 15 8 8 0 1.75
app/controllers/api_controller.rb 0.00 % 9 7 0 7 0.00
app/controllers/api_error_controller.rb 80.00 % 9 5 4 1 0.80
app/controllers/application_controller.rb 80.00 % 26 15 12 3 103.47
app/controllers/component_previews_controller.rb 100.00 % 4 3 3 0 1.00
app/controllers/concerns/authentication.rb 100.00 % 20 11 11 0 1190.45
app/controllers/concerns/basic_authenticable.rb 64.29 % 31 14 9 5 109.71
app/controllers/concerns/copy_course_content.rb 100.00 % 38 20 20 0 17.65
app/controllers/concerns/emits_request_events.rb 100.00 % 22 13 13 0 129.77
app/controllers/concerns/error_handlers/base.rb 93.75 % 38 16 15 1 6.50
app/controllers/concerns/error_handlers/pagy.rb 100.00 % 13 7 7 0 4.86
app/controllers/concerns/pagy_pagination.rb 100.00 % 60 30 30 0 26.83
app/controllers/concerns/study_mode_vacancy_mapper.rb 37.50 % 16 8 3 5 1.50
app/controllers/concerns/success_message.rb 100.00 % 15 8 8 0 5.25
app/controllers/concerns/validate_jsonapi_type.rb 0.00 % 15 9 0 9 0.00
app/controllers/errors_controller.rb 0.00 % 15 12 0 12 0.00
app/controllers/heartbeat_controller.rb 91.67 % 54 24 22 2 4.58
app/controllers/magic_link_sessions_controller.rb 100.00 % 82 34 34 0 1.50
app/controllers/magic_links_controller.rb 100.00 % 29 15 15 0 1.40
app/controllers/pages_controller.rb 88.89 % 24 9 8 1 0.89
app/controllers/personas_controller.rb 0.00 % 9 7 0 7 0.00
app/controllers/public_api_controller.rb 100.00 % 6 5 5 0 4.00
app/controllers/publish/access_requests_controller.rb 100.00 % 33 19 19 0 1.89
app/controllers/publish/courses/accredited_body_controller.rb 56.16 % 157 73 41 32 1.18
app/controllers/publish/courses/age_range_controller.rb 97.73 % 91 44 43 1 6.14
app/controllers/publish/courses/applications_open_controller.rb 92.59 % 78 27 25 2 3.89
app/controllers/publish/courses/apprenticeship_controller.rb 100.00 % 17 9 9 0 5.67
app/controllers/publish/courses/base_funding_type_controller.rb 0.00 % 39 32 0 32 0.00
app/controllers/publish/courses/course_information_controller.rb 100.00 % 52 23 23 0 7.17
app/controllers/publish/courses/degrees/grade_controller.rb 100.00 % 42 23 23 0 2.09
app/controllers/publish/courses/degrees/start_controller.rb 100.00 % 46 27 27 0 3.26
app/controllers/publish/courses/degrees/subject_requirements_controller.rb 100.00 % 59 32 32 0 4.31
app/controllers/publish/courses/deletions_controller.rb 100.00 % 47 21 21 0 1.95
app/controllers/publish/courses/fee_or_salary_controller.rb 100.00 % 17 9 9 0 5.56
app/controllers/publish/courses/fees_controller.rb 0.00 % 44 35 0 35 0.00
app/controllers/publish/courses/gcse_requirements_controller.rb 100.00 % 73 33 33 0 5.06
app/controllers/publish/courses/level_controller.rb 100.00 % 17 9 9 0 10.56
app/controllers/publish/courses/modern_languages_controller.rb 93.33 % 121 60 56 4 7.18
app/controllers/publish/courses/outcome_controller.rb 89.47 % 77 38 34 4 3.89
app/controllers/publish/courses/requirements_controller.rb 96.00 % 55 25 24 1 3.64
app/controllers/publish/courses/salary_controller.rb 0.00 % 40 32 0 32 0.00
app/controllers/publish/courses/sites_controller.rb 82.61 % 88 46 38 8 2.80
app/controllers/publish/courses/start_date_controller.rb 100.00 % 17 9 9 0 2.78
app/controllers/publish/courses/study_mode_controller.rb 100.00 % 46 22 22 0 3.64
app/controllers/publish/courses/subjects_controller.rb 91.84 % 103 49 45 4 4.65
app/controllers/publish/courses/vacancies_controller.rb 100.00 % 48 21 21 0 7.33
app/controllers/publish/courses/withdrawals_controller.rb 100.00 % 64 29 29 0 2.17
app/controllers/publish/courses_controller.rb 91.18 % 139 68 62 6 8.76
app/controllers/publish/notifications_controller.rb 96.00 % 47 25 24 1 1.36
app/controllers/publish/providers/access_requests_controller.rb 0.00 % 36 28 0 28 0.00
app/controllers/publish/providers/allocations_controller.rb 91.53 % 122 59 54 5 9.14
app/controllers/publish/providers/contacts_controller.rb 100.00 % 34 16 16 0 1.38
app/controllers/publish/providers/edit_initial_allocations_controller.rb 100.00 % 68 32 32 0 5.31
app/controllers/publish/providers/locations_controller.rb 93.55 % 60 31 29 2 1.16
app/controllers/publish/providers/visas_controller.rb 100.00 % 38 18 18 0 1.39
app/controllers/publish/providers_controller.rb 85.96 % 117 57 49 8 46.67
app/controllers/publish/publish_controller.rb 100.00 % 49 28 28 0 324.75
app/controllers/publish/recruitment_cycles_controller.rb 0.00 % 14 12 0 12 0.00
app/controllers/publish/rollover_controller.rb 100.00 % 15 6 6 0 1.00
app/controllers/publish/rollover_recruitment_controller.rb 100.00 % 15 6 6 0 1.00
app/controllers/publish/terms_controller.rb 100.00 % 25 13 13 0 2.92
app/controllers/publish/training_providers/course_exports_controller.rb 0.00 % 25 21 0 21 0.00
app/controllers/publish/training_providers/courses_controller.rb 100.00 % 26 11 11 0 1.00
app/controllers/publish/training_providers_controller.rb 100.00 % 10 6 6 0 1.83
app/controllers/publish/users_controller.rb 0.00 % 9 8 0 8 0.00
app/controllers/reporting_controller.rb 100.00 % 15 7 7 0 1.00
app/controllers/sessions_controller.rb 95.45 % 42 22 21 1 62.77
app/controllers/sign_in_controller.rb 100.00 % 11 6 6 0 78.00
app/controllers/support/allocation_uplifts_controller.rb 92.00 % 47 25 23 2 1.56
app/controllers/support/allocations_controller.rb 100.00 % 25 13 13 0 3.69
app/controllers/support/courses_controller.rb 100.00 % 57 32 32 0 6.75
app/controllers/support/data_exports_controller.rb 0.00 % 15 14 0 14 0.00
app/controllers/support/locations_controller.rb 90.91 % 71 33 30 3 2.39
app/controllers/support/providers_controller.rb 100.00 % 95 42 42 0 5.55
app/controllers/support/support_controller.rb 100.00 % 15 9 9 0 16.44
app/controllers/support/user_permissions_controller.rb 100.00 % 31 16 16 0 1.56
app/controllers/support/users/providers_controller.rb 100.00 % 21 12 12 0 1.58
app/controllers/support/users_controller.rb 82.86 % 67 35 29 6 2.83
app/decorators/application_decorator.rb 100.00 % 37 16 16 0 27.31
app/decorators/course_decorator.rb 89.94 % 390 169 152 17 14.46
app/decorators/provider_decorator.rb 0.00 % 15 12 0 12 0.00
app/deserializers/api/v3/deserializable_access_request.rb 0.00 % 12 12 0 12 0.00
app/deserializers/api/v3/deserializable_contact.rb 0.00 % 7 7 0 7 0.00
app/deserializers/api/v3/deserializable_course.rb 100.00 % 56 11 11 0 4.18
app/deserializers/api/v3/deserializable_provider.rb 100.00 % 45 11 11 0 3.27
app/deserializers/api/v3/deserializable_site.rb 100.00 % 15 4 4 0 1.00
app/flows/edit_initial_request_flow.rb 96.00 % 122 50 48 2 11.74
app/flows/initial_request_flow.rb 97.96 % 222 98 96 2 14.41
app/forms/publish/about_your_organisation_form.rb 94.87 % 85 39 37 2 3.18
app/forms/publish/access_request_form.rb 0.00 % 39 31 0 31 0.00
app/forms/publish/age_range_form.rb 100.00 % 111 52 52 0 5.00
app/forms/publish/allocations/edit_initial/number_of_places_form.rb 100.00 % 15 7 7 0 2.00
app/forms/publish/allocations/edit_initial/request_type_form.rb 100.00 % 13 7 7 0 1.00
app/forms/publish/authentication/magic_link_form.rb 100.00 % 26 14 14 0 1.14
app/forms/publish/base_course_form.rb 93.75 % 29 16 15 1 8.56
app/forms/publish/base_model_form.rb 96.15 % 45 26 25 1 35.27
app/forms/publish/base_provider_form.rb 100.00 % 5 3 3 0 4.00
app/forms/publish/course_deletion_form.rb 100.00 % 33 16 16 0 1.31
app/forms/publish/course_fee_form.rb 100.00 % 42 15 15 0 1.60
app/forms/publish/course_information_form.rb 100.00 % 39 20 20 0 3.45
app/forms/publish/course_location_form.rb 100.00 % 60 29 29 0 2.55
app/forms/publish/course_requirement_form.rb 100.00 % 21 10 10 0 1.90
app/forms/publish/course_salary_form.rb 100.00 % 25 12 12 0 1.42
app/forms/publish/course_study_mode_form.rb 100.00 % 19 9 9 0 1.33
app/forms/publish/course_subjects_form.rb 95.65 % 46 23 22 1 3.78
app/forms/publish/course_vacancies_form.rb 100.00 % 127 50 50 0 9.08
app/forms/publish/course_withdrawal_form.rb 100.00 % 39 20 20 0 2.65
app/forms/publish/degree_grade_form.rb 100.00 % 20 10 10 0 1.60
app/forms/publish/degree_start_form.rb 95.45 % 41 22 21 1 2.50
app/forms/publish/funding_type_form_methods.rb 93.75 % 40 16 15 1 4.88
app/forms/publish/gcse_requirements_form.rb 100.00 % 63 28 28 0 4.57
app/forms/publish/initial_request_form.rb 100.00 % 37 18 18 0 6.50
app/forms/publish/interruption/accept_terms_form.rb 100.00 % 25 12 12 0 2.83
app/forms/publish/location_form.rb 91.30 % 46 23 21 2 1.87
app/forms/publish/notification_form.rb 94.44 % 37 18 17 1 1.56
app/forms/publish/provider_contact_form.rb 100.00 % 23 12 12 0 1.25
app/forms/publish/provider_visa_form.rb 100.00 % 19 9 9 0 2.56
app/forms/publish/repeat_request_form.rb 100.00 % 9 5 5 0 2.00
app/forms/publish/subject_requirement_form.rb 100.00 % 36 17 17 0 1.94
app/forms/support/edit_course_form.rb 100.00 % 126 56 56 0 13.93
app/helpers/age_range_errors_view_helper.rb 83.33 % 22 12 10 2 5.83
app/helpers/allocation_cycle_helper.rb 100.00 % 9 5 5 0 44.20
app/helpers/application_helper.rb 89.19 % 83 37 33 4 146.76
app/helpers/breadcrumb_helper.rb 91.84 % 90 49 45 4 24.98
app/helpers/layout_helper.rb 100.00 % 13 6 6 0 493.33
app/helpers/location_helper.rb 100.00 % 5 3 3 0 5.33
app/helpers/navigation_bar_helper.rb 100.00 % 5 3 3 0 17.00
app/helpers/provider_helper.rb 73.08 % 51 26 19 7 4.69
app/helpers/publish_helper.rb 100.00 % 41 14 14 0 13.86
app/helpers/recruitment_cycle_helper.rb 85.71 % 15 7 6 1 3.71
app/helpers/support/time_helper.rb 100.00 % 9 4 4 0 5.75
app/helpers/support_email_helper.rb 100.00 % 7 3 3 0 76.67
app/helpers/user_helper.rb 80.00 % 11 5 4 1 4.20
app/helpers/vacancy_helper.rb 92.86 % 31 14 13 1 10.57
app/helpers/view_helper.rb 97.14 % 151 35 34 1 165.29
app/jobs/application_job.rb 100.00 % 5 3 3 0 4.00
app/jobs/geocode_job.rb 100.00 % 11 7 7 0 2.29
app/jobs/save_statistic_job.rb 100.00 % 7 4 4 0 1.00
app/jobs/send_event_to_big_query_job.rb 100.00 % 13 9 9 0 2.00
app/lib/api_constraint.rb 100.00 % 7 3 3 0 4.33
app/lib/big_query/entity_event.rb 100.00 % 51 24 24 0 28274.38
app/lib/big_query/request_event.rb 100.00 % 58 22 22 0 5.95
app/lib/govuk/markdown_renderer.rb 66.67 % 61 21 14 7 1.95
app/lib/time_format.rb 100.00 % 22 9 9 0 40.00
app/mailers/application_mailer.rb 0.00 % 4 4 0 4 0.00
app/mailers/course_publish_email_mailer.rb 100.00 % 27 9 9 0 4.56
app/mailers/course_sites_update_email_mailer.rb 100.00 % 32 9 9 0 5.11
app/mailers/course_subjects_updated_email_mailer.rb 100.00 % 38 11 11 0 6.55
app/mailers/course_update_email_mailer.rb 100.00 % 46 14 14 0 11.29
app/mailers/course_vacancies/updated_mailer.rb 100.00 % 45 18 18 0 7.44
app/mailers/course_withdraw_email_mailer.rb 100.00 % 16 6 6 0 4.00
app/mailers/magic_link_email_mailer.rb 100.00 % 18 8 8 0 3.00
app/mailers/welcome_email_mailer.rb 100.00 % 18 8 8 0 2.50
app/models/access_request.rb 100.00 % 33 13 13 0 22.08
app/models/accrediting_provider_enrichment.rb 85.71 % 40 21 18 3 488.62
app/models/allocation.rb 100.00 % 52 23 23 0 15.39
app/models/allocation_uplift.rb 100.00 % 7 4 4 0 4.00
app/models/api_spec.rb 0.00 % 31 21 0 21 0.00
app/models/api_spec/public.rb 0.00 % 17 13 0 13 0.00
app/models/application_record.rb 100.00 % 4 3 3 0 4.00
app/models/concerns/changed_at.rb 100.00 % 16 6 6 0 4.67
app/models/concerns/courses/edit_options.rb 100.00 % 35 14 14 0 16.79
app/models/concerns/courses/edit_options/age_range_concern.rb 100.00 % 30 9 9 0 46.22
app/models/concerns/courses/edit_options/applications_open_concern.rb 100.00 % 16 7 7 0 29.86
app/models/concerns/courses/edit_options/entry_requirements_concern.rb 100.00 % 18 8 8 0 141.88
app/models/concerns/courses/edit_options/is_send_concern.rb 100.00 % 16 7 7 0 29.86
app/models/concerns/courses/edit_options/qualification_concern.rb 100.00 % 32 15 15 0 221.20
app/models/concerns/courses/edit_options/start_date_concern.rb 100.00 % 43 10 10 0 87.30
app/models/concerns/courses/edit_options/study_mode_concern.rb 100.00 % 16 7 7 0 29.57
app/models/concerns/courses/edit_options/subjects_concern.rb 100.00 % 24 11 11 0 53.18
app/models/concerns/emits_entity_events.rb 100.00 % 53 26 26 0 4741.19
app/models/concerns/postcode_normalize.rb 100.00 % 12 7 7 0 211.43
app/models/concerns/publish/course_basic_detail_concern.rb 83.33 % 201 96 80 16 41.10
app/models/concerns/region_code.rb 100.00 % 22 4 4 0 5.00
app/models/concerns/touch_course.rb 100.00 % 13 7 7 0 159.43
app/models/concerns/touch_provider.rb 100.00 % 13 7 7 0 527.86
app/models/concerns/with_qualifications.rb 100.00 % 78 20 20 0 478.05
app/models/contact.rb 100.00 % 23 10 10 0 4.00
app/models/course.rb 96.64 % 962 476 460 16 275.41
app/models/course_enrichment.rb 100.00 % 106 40 40 0 164.73
app/models/course_subject.rb 100.00 % 9 6 6 0 4.00
app/models/financial_incentive.rb 100.00 % 3 2 2 0 4.00
app/models/interrupt_page_acknowledgement.rb 100.00 % 8 5 5 0 3.00
app/models/organisation.rb 100.00 % 16 9 9 0 3.67
app/models/organisation_user.rb 100.00 % 6 4 4 0 4.00
app/models/provider.rb 98.68 % 370 151 149 2 305.89
app/models/provider_ucas_preference.rb 100.00 % 27 8 8 0 3.38
app/models/recruitment_cycle.rb 78.95 % 69 38 30 8 389.92
app/models/required_qualifications_summary.rb 97.67 % 88 43 42 1 33.65
app/models/site.rb 100.00 % 83 43 43 0 180.95
app/models/site_status.rb 98.33 % 124 60 59 1 128.52
app/models/statistic.rb 100.00 % 2 1 1 0 1.00
app/models/subject.rb 100.00 % 24 14 14 0 214.43
app/models/subject_area.rb 100.00 % 5 4 4 0 6.25
app/models/subjects/discontinued_subject.rb 100.00 % 2 1 1 0 4.00
app/models/subjects/further_education_subject.rb 100.00 % 5 3 3 0 5.00
app/models/subjects/modern_languages_subject.rb 100.00 % 2 1 1 0 4.00
app/models/subjects/primary_subject.rb 100.00 % 2 1 1 0 4.00
app/models/subjects/secondary_subject.rb 100.00 % 9 5 5 0 907.80
app/models/user.rb 100.00 % 107 53 53 0 67.45
app/models/user_notification.rb 100.00 % 19 8 8 0 8.25
app/models/user_notification_preferences.rb 100.00 % 68 32 32 0 14.63
app/models/user_permission.rb 100.00 % 4 3 3 0 4.00
app/models/user_session.rb 96.55 % 68 29 28 1 543.93
app/policies/access_request_policy.rb 100.00 % 22 14 14 0 4.00
app/policies/allocation_policy.rb 97.06 % 62 34 33 1 9.06
app/policies/contact_policy.rb 100.00 % 26 14 14 0 2.71
app/policies/course_policy.rb 97.50 % 94 40 39 1 48.65
app/policies/organisation_policy.rb 100.00 % 14 8 8 0 1.38
app/policies/provider_policy.rb 92.16 % 105 51 47 4 48.76
app/policies/recruitment_cycle_policy.rb 100.00 % 30 16 16 0 1.31
app/policies/site_policy.rb 100.00 % 19 11 11 0 2.36
app/policies/site_status_policy.rb 100.00 % 12 7 7 0 1.43
app/policies/user_notification_preferences_policy.rb 100.00 % 22 12 12 0 1.92
app/policies/user_policy.rb 100.00 % 23 15 15 0 2.53
app/serializers/api/public/v1/serializable_course.rb 100.00 % 144 67 67 0 53.03
app/serializers/api/public/v1/serializable_location.rb 100.00 % 55 25 25 0 13.36
app/serializers/api/public/v1/serializable_location_status.rb 100.00 % 22 11 11 0 3.64
app/serializers/api/public/v1/serializable_provider.rb 100.00 % 62 25 25 0 25.28
app/serializers/api/public/v1/serializable_provider_suggestion.rb 100.00 % 20 10 10 0 1.80
app/serializers/api/public/v1/serializable_recruitment_cycle.rb 100.00 % 16 8 8 0 4.75
app/serializers/api/public/v1/serializable_subject.rb 100.00 % 33 17 17 0 56.47
app/serializers/api/public/v1/serializable_subject_area.rb 100.00 % 13 7 7 0 4.00
app/serializers/api/public/v1/serializer_service.rb 100.00 % 25 7 7 0 12.43
app/serializers/api/v3/serializable_course.rb 100.00 % 81 43 43 0 69.37
app/serializers/api/v3/serializable_provider.rb 100.00 % 19 8 8 0 12.63
app/serializers/api/v3/serializable_recruitment_cycle.rb 100.00 % 11 6 6 0 4.00
app/serializers/api/v3/serializable_site.rb 100.00 % 17 8 8 0 6.88
app/serializers/api/v3/serializable_site_status.rb 100.00 % 12 7 7 0 4.00
app/serializers/api/v3/serializable_subject.rb 100.00 % 27 14 14 0 32.29
app/serializers/api/v3/serializable_subject_area.rb 100.00 % 11 7 7 0 4.00
app/serializers/jsonapi_cache_key_helper.rb 100.00 % 5 3 3 0 16.33
app/serializers/jsonapi_course_cache_key_helper.rb 100.00 % 7 3 3 0 22.33
app/services/access_request_approval_service.rb 100.00 % 22 13 13 0 8.62
app/services/allocation_copy_data_service.rb 82.35 % 80 34 28 6 1.32
app/services/allocation_importer_service.rb 95.24 % 46 21 20 1 2.62
app/services/allocation_reporting_service.rb 100.00 % 36 15 15 0 8.40
app/services/allocations/create.rb 100.00 % 52 28 28 0 3.39
app/services/allocations/update.rb 100.00 % 36 19 19 0 6.58
app/services/authentication_service.rb 97.78 % 182 90 88 2 53.24
app/services/course_attribute_formatter_service.rb 100.00 % 67 36 36 0 8.17
app/services/course_reporting_service.rb 100.00 % 97 42 42 0 105.67
app/services/course_search_service.rb 99.10 % 242 111 110 1 37.28
app/services/course_serializers_service.rb 100.00 % 43 15 15 0 58.40
app/services/courses/assign_program_type_service.rb 100.00 % 33 17 17 0 24.76
app/services/courses/assign_subjects_service.rb 100.00 % 54 30 30 0 70.97
app/services/courses/assignable_master_subject_service.rb 100.00 % 24 11 11 0 86.91
app/services/courses/assignable_subject_service.rb 100.00 % 28 13 13 0 1.92
app/services/courses/content_status_service.rb 100.00 % 23 16 16 0 246.31
app/services/courses/copy.rb 100.00 % 63 19 19 0 6.00
app/services/courses/copy_to_provider_service.rb 100.00 % 58 35 35 0 9.71
app/services/courses/creation_service.rb 100.00 % 67 36 36 0 75.78
app/services/courses/fetch.rb 90.91 % 30 11 10 1 36.36
app/services/courses/generate_course_name_service.rb 100.00 % 76 40 40 0 70.53
app/services/courses/validate_custom_age_range_service.rb 100.00 % 33 19 19 0 1304.47
app/services/csv_imports/fake_providers_import.rb 100.00 % 35 17 17 0 4.53
app/services/enrichments/copy_to_course_service.rb 100.00 % 10 7 7 0 3.71
app/services/exports/accredited_course_list.rb 90.00 % 52 20 18 2 1.35
app/services/feature_service.rb 100.00 % 21 10 10 0 7642.90
app/services/generate_and_send_magic_link_service.rb 100.00 % 27 14 14 0 2.50
app/services/geocoder_service.rb 100.00 % 17 10 10 0 3.20
app/services/notification_service/course_published.rb 100.00 % 36 18 18 0 4.17
app/services/notification_service/course_sites_updated.rb 100.00 % 40 17 17 0 3.76
app/services/notification_service/course_subjects_updated.rb 100.00 % 44 20 20 0 4.40
app/services/notification_service/course_updated.rb 100.00 % 46 19 19 0 5.16
app/services/notification_service/course_vacancies_updated.rb 100.00 % 84 40 40 0 6.63
app/services/notification_service/course_withdrawn.rb 100.00 % 36 17 17 0 3.94
app/services/performance_dashboard_service.rb 0.00 % 115 86 0 86 0.00
app/services/provider_enrichments/rollover_enrichment_to_provider_service.rb 0.00 % 10 10 0 10 0.00
app/services/provider_reporting_service.rb 100.00 % 57 23 23 0 57.00
app/services/providers/copy_to_recruitment_cycle_service.rb 100.00 % 72 42 42 0 10.19
app/services/providers/create_fake_provider_service.rb 100.00 % 72 31 31 0 7.84
app/services/providers/generate_course_code_service.rb 100.00 % 17 9 9 0 5.33
app/services/providers/generate_unique_course_code_service.rb 100.00 % 15 8 8 0 6.00
app/services/providers/visa_sponsorship_service.rb 100.00 % 13 7 7 0 3.71
app/services/publish/course_creation_step_service.rb 80.00 % 136 40 32 8 49.05
app/services/publish_reporting_service.rb 100.00 % 127 46 46 0 10.04
app/services/record_first_login_service.rb 100.00 % 7 4 4 0 2.00
app/services/recruitment_cycle_creation_service.rb 90.91 % 18 11 10 1 0.91
app/services/rollover_reporting_service.rb 100.00 % 75 31 31 0 5.71
app/services/rollover_service.rb 100.00 % 70 28 28 0 6.54
app/services/send_welcome_email_service.rb 100.00 % 25 11 11 0 7.00
app/services/serialize_course_service.rb 100.00 % 10 6 6 0 2.00
app/services/service_container.rb 77.27 % 48 22 17 5 1.23
app/services/service_pattern.rb 90.91 % 20 11 10 1 144.45
app/services/sites/code_generator.rb 100.00 % 44 23 23 0 11.30
app/services/sites/copy_to_course_service.rb 100.00 % 15 5 5 0 2.80
app/services/sites/copy_to_provider_service.rb 100.00 % 18 12 12 0 2.92
app/services/statistic_service.rb 100.00 % 15 5 5 0 5.40
app/services/subjects/creator_service.rb 100.00 % 105 24 24 0 830.17
app/services/subjects/financial_incentive_creator_service.rb 100.00 % 80 13 13 0 587.38
app/services/subjects/financial_incentive_set_subject_knowledge_enhancement_course_available_service.rb 100.00 % 20 11 11 0 174.18
app/services/subjects/subject_area_creator_service.rb 100.00 % 15 10 10 0 191.20
app/services/support/data_exports/base.rb 91.67 % 22 12 11 1 1.00
app/services/support/data_exports/data_export.rb 100.00 % 19 8 8 0 1.38
app/services/support/data_exports/users_export.rb 100.00 % 38 14 14 0 1.36
app/services/support/filter.rb 96.88 % 62 32 31 1 20.19
app/services/token/decode_service.rb 100.00 % 54 18 18 0 16.89
app/services/user_associations_service/create.rb 100.00 % 51 27 27 0 3.89
app/services/user_associations_service/delete.rb 100.00 % 38 20 20 0 3.15
app/services/user_sessions/update.rb 100.00 % 32 14 14 0 116.07
app/services/v3/course_search_service.rb 100.00 % 235 112 112 0 41.10
app/services/vacancy_status_determination_service.rb 100.00 % 52 27 27 0 13.48
app/validators/email_address_validator.rb 100.00 % 9 5 5 0 699.00
app/validators/phone_validator.rb 100.00 % 15 8 8 0 869.25
app/validators/postcode_validator.rb 100.00 % 10 6 6 0 417.67
app/validators/reference_number_format_validator.rb 100.00 % 9 5 5 0 3101.40
app/validators/unique_course_validator.rb 100.00 % 25 13 13 0 74.00
app/validators/words_count_validator.rb 100.00 % 16 7 7 0 6142.00
app/view_objects/allocations_view.rb 100.00 % 215 104 104 0 9.33
app/view_objects/notifications_view.rb 100.00 % 36 18 18 0 2.78
lib/rack/request_output.rb 0.00 % 13 11 0 11 0.00

Controllers ( 83.43% covered at 20.02 hits/line )

94 files in total.
2028 relevant lines, 1692 lines covered and 336 lines missed. ( 83.43% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/controllers/api/public/v1/application_controller.rb 100.00 % 9 5 5 0 4.00
app/controllers/api/public/v1/courses_controller.rb 100.00 % 36 14 14 0 5.86
app/controllers/api/public/v1/provider_suggestions_controller.rb 100.00 % 33 13 13 0 2.15
app/controllers/api/public/v1/providers/courses/locations_controller.rb 100.00 % 44 21 21 0 3.29
app/controllers/api/public/v1/providers/courses_controller.rb 100.00 % 44 20 20 0 5.30
app/controllers/api/public/v1/providers/locations_controller.rb 100.00 % 33 16 16 0 2.81
app/controllers/api/public/v1/providers_controller.rb 100.00 % 72 33 33 0 9.88
app/controllers/api/public/v1/subject_areas_controller.rb 100.00 % 15 6 6 0 2.50
app/controllers/api/public/v1/subjects_controller.rb 100.00 % 27 13 13 0 1.92
app/controllers/api/system/application_controller.rb 100.00 % 16 7 7 0 1.29
app/controllers/api/v3/accredited_provider_training_providers_controller.rb 85.71 % 91 42 36 6 7.60
app/controllers/api/v3/application_controller.rb 88.24 % 67 34 30 4 21.85
app/controllers/api/v3/courses_controller.rb 100.00 % 69 28 28 0 16.75
app/controllers/api/v3/provider_suggestions_controller.rb 90.91 % 27 11 10 1 2.18
app/controllers/api/v3/providers_controller.rb 100.00 % 56 22 22 0 7.64
app/controllers/api/v3/subject_areas_controller.rb 100.00 % 9 5 5 0 1.40
app/controllers/api/v3/subjects_controller.rb 100.00 % 15 8 8 0 1.75
app/controllers/api_controller.rb 0.00 % 9 7 0 7 0.00
app/controllers/api_error_controller.rb 80.00 % 9 5 4 1 0.80
app/controllers/application_controller.rb 80.00 % 26 15 12 3 103.47
app/controllers/component_previews_controller.rb 100.00 % 4 3 3 0 1.00
app/controllers/concerns/authentication.rb 100.00 % 20 11 11 0 1190.45
app/controllers/concerns/basic_authenticable.rb 64.29 % 31 14 9 5 109.71
app/controllers/concerns/copy_course_content.rb 100.00 % 38 20 20 0 17.65
app/controllers/concerns/emits_request_events.rb 100.00 % 22 13 13 0 129.77
app/controllers/concerns/error_handlers/base.rb 93.75 % 38 16 15 1 6.50
app/controllers/concerns/error_handlers/pagy.rb 100.00 % 13 7 7 0 4.86
app/controllers/concerns/pagy_pagination.rb 100.00 % 60 30 30 0 26.83
app/controllers/concerns/study_mode_vacancy_mapper.rb 37.50 % 16 8 3 5 1.50
app/controllers/concerns/success_message.rb 100.00 % 15 8 8 0 5.25
app/controllers/concerns/validate_jsonapi_type.rb 0.00 % 15 9 0 9 0.00
app/controllers/errors_controller.rb 0.00 % 15 12 0 12 0.00
app/controllers/heartbeat_controller.rb 91.67 % 54 24 22 2 4.58
app/controllers/magic_link_sessions_controller.rb 100.00 % 82 34 34 0 1.50
app/controllers/magic_links_controller.rb 100.00 % 29 15 15 0 1.40
app/controllers/pages_controller.rb 88.89 % 24 9 8 1 0.89
app/controllers/personas_controller.rb 0.00 % 9 7 0 7 0.00
app/controllers/public_api_controller.rb 100.00 % 6 5 5 0 4.00
app/controllers/publish/access_requests_controller.rb 100.00 % 33 19 19 0 1.89
app/controllers/publish/courses/accredited_body_controller.rb 56.16 % 157 73 41 32 1.18
app/controllers/publish/courses/age_range_controller.rb 97.73 % 91 44 43 1 6.14
app/controllers/publish/courses/applications_open_controller.rb 92.59 % 78 27 25 2 3.89
app/controllers/publish/courses/apprenticeship_controller.rb 100.00 % 17 9 9 0 5.67
app/controllers/publish/courses/base_funding_type_controller.rb 0.00 % 39 32 0 32 0.00
app/controllers/publish/courses/course_information_controller.rb 100.00 % 52 23 23 0 7.17
app/controllers/publish/courses/degrees/grade_controller.rb 100.00 % 42 23 23 0 2.09
app/controllers/publish/courses/degrees/start_controller.rb 100.00 % 46 27 27 0 3.26
app/controllers/publish/courses/degrees/subject_requirements_controller.rb 100.00 % 59 32 32 0 4.31
app/controllers/publish/courses/deletions_controller.rb 100.00 % 47 21 21 0 1.95
app/controllers/publish/courses/fee_or_salary_controller.rb 100.00 % 17 9 9 0 5.56
app/controllers/publish/courses/fees_controller.rb 0.00 % 44 35 0 35 0.00
app/controllers/publish/courses/gcse_requirements_controller.rb 100.00 % 73 33 33 0 5.06
app/controllers/publish/courses/level_controller.rb 100.00 % 17 9 9 0 10.56
app/controllers/publish/courses/modern_languages_controller.rb 93.33 % 121 60 56 4 7.18
app/controllers/publish/courses/outcome_controller.rb 89.47 % 77 38 34 4 3.89
app/controllers/publish/courses/requirements_controller.rb 96.00 % 55 25 24 1 3.64
app/controllers/publish/courses/salary_controller.rb 0.00 % 40 32 0 32 0.00
app/controllers/publish/courses/sites_controller.rb 82.61 % 88 46 38 8 2.80
app/controllers/publish/courses/start_date_controller.rb 100.00 % 17 9 9 0 2.78
app/controllers/publish/courses/study_mode_controller.rb 100.00 % 46 22 22 0 3.64
app/controllers/publish/courses/subjects_controller.rb 91.84 % 103 49 45 4 4.65
app/controllers/publish/courses/vacancies_controller.rb 100.00 % 48 21 21 0 7.33
app/controllers/publish/courses/withdrawals_controller.rb 100.00 % 64 29 29 0 2.17
app/controllers/publish/courses_controller.rb 91.18 % 139 68 62 6 8.76
app/controllers/publish/notifications_controller.rb 96.00 % 47 25 24 1 1.36
app/controllers/publish/providers/access_requests_controller.rb 0.00 % 36 28 0 28 0.00
app/controllers/publish/providers/allocations_controller.rb 91.53 % 122 59 54 5 9.14
app/controllers/publish/providers/contacts_controller.rb 100.00 % 34 16 16 0 1.38
app/controllers/publish/providers/edit_initial_allocations_controller.rb 100.00 % 68 32 32 0 5.31
app/controllers/publish/providers/locations_controller.rb 93.55 % 60 31 29 2 1.16
app/controllers/publish/providers/visas_controller.rb 100.00 % 38 18 18 0 1.39
app/controllers/publish/providers_controller.rb 85.96 % 117 57 49 8 46.67
app/controllers/publish/publish_controller.rb 100.00 % 49 28 28 0 324.75
app/controllers/publish/recruitment_cycles_controller.rb 0.00 % 14 12 0 12 0.00
app/controllers/publish/rollover_controller.rb 100.00 % 15 6 6 0 1.00
app/controllers/publish/rollover_recruitment_controller.rb 100.00 % 15 6 6 0 1.00
app/controllers/publish/terms_controller.rb 100.00 % 25 13 13 0 2.92
app/controllers/publish/training_providers/course_exports_controller.rb 0.00 % 25 21 0 21 0.00
app/controllers/publish/training_providers/courses_controller.rb 100.00 % 26 11 11 0 1.00
app/controllers/publish/training_providers_controller.rb 100.00 % 10 6 6 0 1.83
app/controllers/publish/users_controller.rb 0.00 % 9 8 0 8 0.00
app/controllers/reporting_controller.rb 100.00 % 15 7 7 0 1.00
app/controllers/sessions_controller.rb 95.45 % 42 22 21 1 62.77
app/controllers/sign_in_controller.rb 100.00 % 11 6 6 0 78.00
app/controllers/support/allocation_uplifts_controller.rb 92.00 % 47 25 23 2 1.56
app/controllers/support/allocations_controller.rb 100.00 % 25 13 13 0 3.69
app/controllers/support/courses_controller.rb 100.00 % 57 32 32 0 6.75
app/controllers/support/data_exports_controller.rb 0.00 % 15 14 0 14 0.00
app/controllers/support/locations_controller.rb 90.91 % 71 33 30 3 2.39
app/controllers/support/providers_controller.rb 100.00 % 95 42 42 0 5.55
app/controllers/support/support_controller.rb 100.00 % 15 9 9 0 16.44
app/controllers/support/user_permissions_controller.rb 100.00 % 31 16 16 0 1.56
app/controllers/support/users/providers_controller.rb 100.00 % 21 12 12 0 1.58
app/controllers/support/users_controller.rb 82.86 % 67 35 29 6 2.83

Channels ( 100.0% covered at 0.0 hits/line )

0 files in total.
0 relevant lines, 0 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line

Models ( 94.18% covered at 287.09 hits/line )

51 files in total.
1409 relevant lines, 1327 lines covered and 82 lines missed. ( 94.18% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/models/access_request.rb 100.00 % 33 13 13 0 22.08
app/models/accrediting_provider_enrichment.rb 85.71 % 40 21 18 3 488.62
app/models/allocation.rb 100.00 % 52 23 23 0 15.39
app/models/allocation_uplift.rb 100.00 % 7 4 4 0 4.00
app/models/api_spec.rb 0.00 % 31 21 0 21 0.00
app/models/api_spec/public.rb 0.00 % 17 13 0 13 0.00
app/models/application_record.rb 100.00 % 4 3 3 0 4.00
app/models/concerns/changed_at.rb 100.00 % 16 6 6 0 4.67
app/models/concerns/courses/edit_options.rb 100.00 % 35 14 14 0 16.79
app/models/concerns/courses/edit_options/age_range_concern.rb 100.00 % 30 9 9 0 46.22
app/models/concerns/courses/edit_options/applications_open_concern.rb 100.00 % 16 7 7 0 29.86
app/models/concerns/courses/edit_options/entry_requirements_concern.rb 100.00 % 18 8 8 0 141.88
app/models/concerns/courses/edit_options/is_send_concern.rb 100.00 % 16 7 7 0 29.86
app/models/concerns/courses/edit_options/qualification_concern.rb 100.00 % 32 15 15 0 221.20
app/models/concerns/courses/edit_options/start_date_concern.rb 100.00 % 43 10 10 0 87.30
app/models/concerns/courses/edit_options/study_mode_concern.rb 100.00 % 16 7 7 0 29.57
app/models/concerns/courses/edit_options/subjects_concern.rb 100.00 % 24 11 11 0 53.18
app/models/concerns/emits_entity_events.rb 100.00 % 53 26 26 0 4741.19
app/models/concerns/postcode_normalize.rb 100.00 % 12 7 7 0 211.43
app/models/concerns/publish/course_basic_detail_concern.rb 83.33 % 201 96 80 16 41.10
app/models/concerns/region_code.rb 100.00 % 22 4 4 0 5.00
app/models/concerns/touch_course.rb 100.00 % 13 7 7 0 159.43
app/models/concerns/touch_provider.rb 100.00 % 13 7 7 0 527.86
app/models/concerns/with_qualifications.rb 100.00 % 78 20 20 0 478.05
app/models/contact.rb 100.00 % 23 10 10 0 4.00
app/models/course.rb 96.64 % 962 476 460 16 275.41
app/models/course_enrichment.rb 100.00 % 106 40 40 0 164.73
app/models/course_subject.rb 100.00 % 9 6 6 0 4.00
app/models/financial_incentive.rb 100.00 % 3 2 2 0 4.00
app/models/interrupt_page_acknowledgement.rb 100.00 % 8 5 5 0 3.00
app/models/organisation.rb 100.00 % 16 9 9 0 3.67
app/models/organisation_user.rb 100.00 % 6 4 4 0 4.00
app/models/provider.rb 98.68 % 370 151 149 2 305.89
app/models/provider_ucas_preference.rb 100.00 % 27 8 8 0 3.38
app/models/recruitment_cycle.rb 78.95 % 69 38 30 8 389.92
app/models/required_qualifications_summary.rb 97.67 % 88 43 42 1 33.65
app/models/site.rb 100.00 % 83 43 43 0 180.95
app/models/site_status.rb 98.33 % 124 60 59 1 128.52
app/models/statistic.rb 100.00 % 2 1 1 0 1.00
app/models/subject.rb 100.00 % 24 14 14 0 214.43
app/models/subject_area.rb 100.00 % 5 4 4 0 6.25
app/models/subjects/discontinued_subject.rb 100.00 % 2 1 1 0 4.00
app/models/subjects/further_education_subject.rb 100.00 % 5 3 3 0 5.00
app/models/subjects/modern_languages_subject.rb 100.00 % 2 1 1 0 4.00
app/models/subjects/primary_subject.rb 100.00 % 2 1 1 0 4.00
app/models/subjects/secondary_subject.rb 100.00 % 9 5 5 0 907.80
app/models/user.rb 100.00 % 107 53 53 0 67.45
app/models/user_notification.rb 100.00 % 19 8 8 0 8.25
app/models/user_notification_preferences.rb 100.00 % 68 32 32 0 14.63
app/models/user_permission.rb 100.00 % 4 3 3 0 4.00
app/models/user_session.rb 96.55 % 68 29 28 1 543.93

Mailers ( 95.4% covered at 5.97 hits/line )

9 files in total.
87 relevant lines, 83 lines covered and 4 lines missed. ( 95.4% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/mailers/application_mailer.rb 0.00 % 4 4 0 4 0.00
app/mailers/course_publish_email_mailer.rb 100.00 % 27 9 9 0 4.56
app/mailers/course_sites_update_email_mailer.rb 100.00 % 32 9 9 0 5.11
app/mailers/course_subjects_updated_email_mailer.rb 100.00 % 38 11 11 0 6.55
app/mailers/course_update_email_mailer.rb 100.00 % 46 14 14 0 11.29
app/mailers/course_vacancies/updated_mailer.rb 100.00 % 45 18 18 0 7.44
app/mailers/course_withdraw_email_mailer.rb 100.00 % 16 6 6 0 4.00
app/mailers/magic_link_email_mailer.rb 100.00 % 18 8 8 0 3.00
app/mailers/welcome_email_mailer.rb 100.00 % 18 8 8 0 2.50

Helpers ( 90.58% covered at 74.09 hits/line )

15 files in total.
223 relevant lines, 202 lines covered and 21 lines missed. ( 90.58% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/helpers/age_range_errors_view_helper.rb 83.33 % 22 12 10 2 5.83
app/helpers/allocation_cycle_helper.rb 100.00 % 9 5 5 0 44.20
app/helpers/application_helper.rb 89.19 % 83 37 33 4 146.76
app/helpers/breadcrumb_helper.rb 91.84 % 90 49 45 4 24.98
app/helpers/layout_helper.rb 100.00 % 13 6 6 0 493.33
app/helpers/location_helper.rb 100.00 % 5 3 3 0 5.33
app/helpers/navigation_bar_helper.rb 100.00 % 5 3 3 0 17.00
app/helpers/provider_helper.rb 73.08 % 51 26 19 7 4.69
app/helpers/publish_helper.rb 100.00 % 41 14 14 0 13.86
app/helpers/recruitment_cycle_helper.rb 85.71 % 15 7 6 1 3.71
app/helpers/support/time_helper.rb 100.00 % 9 4 4 0 5.75
app/helpers/support_email_helper.rb 100.00 % 7 3 3 0 76.67
app/helpers/user_helper.rb 80.00 % 11 5 4 1 4.20
app/helpers/vacancy_helper.rb 92.86 % 31 14 13 1 10.57
app/helpers/view_helper.rb 97.14 % 151 35 34 1 165.29

Jobs ( 100.0% covered at 2.17 hits/line )

4 files in total.
23 relevant lines, 23 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/jobs/application_job.rb 100.00 % 5 3 3 0 4.00
app/jobs/geocode_job.rb 100.00 % 11 7 7 0 2.29
app/jobs/save_statistic_job.rb 100.00 % 7 4 4 0 1.00
app/jobs/send_event_to_big_query_job.rb 100.00 % 13 9 9 0 2.00

Libraries ( 80.0% covered at 7545.89 hits/line )

6 files in total.
90 relevant lines, 72 lines covered and 18 lines missed. ( 80.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/lib/api_constraint.rb 100.00 % 7 3 3 0 4.33
app/lib/big_query/entity_event.rb 100.00 % 51 24 24 0 28274.38
app/lib/big_query/request_event.rb 100.00 % 58 22 22 0 5.95
app/lib/govuk/markdown_renderer.rb 66.67 % 61 21 14 7 1.95
app/lib/time_format.rb 100.00 % 22 9 9 0 40.00
lib/rack/request_output.rb 0.00 % 13 11 0 11 0.00

Ungrouped ( 93.64% covered at 78.85 hits/line )

171 files in total.
3724 relevant lines, 3487 lines covered and 237 lines missed. ( 93.64% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/components/allocation_record_card/view.rb 100.00 % 38 21 21 0 8.00
app/components/degree_preview_component/view.rb 86.67 % 36 15 13 2 1.07
app/components/degree_row_content_component/view.rb 94.44 % 40 18 17 1 10.44
app/components/filters/allocation_attributes/view.rb 100.00 % 12 7 7 0 4.71
app/components/filters/provider_attributes/view.rb 100.00 % 12 7 7 0 9.71
app/components/filters/user_attributes/view.rb 100.00 % 22 12 12 0 10.50
app/components/filters/view.rb 100.00 % 42 22 22 0 16.68
app/components/flash_banner/view.rb 100.00 % 18 9 9 0 335.33
app/components/gcse_preview_component/view.rb 100.00 % 7 3 3 0 1.00
app/components/gcse_requirements_component/view.rb 80.00 % 88 45 36 9 10.84
app/components/gcse_row_content_component/view.rb 100.00 % 6 2 2 0 4.00
app/components/header/view.rb 100.00 % 17 9 9 0 445.78
app/components/navigation_bar/view.rb 100.00 % 28 16 16 0 17.69
app/components/notification_banner/view.rb 100.00 % 74 36 36 0 40.67
app/components/page_title/view.rb 100.00 % 33 17 17 0 155.12
app/components/paginated_filter/view.rb 100.00 % 27 12 12 0 19.83
app/components/paginator/view.rb 100.00 % 80 36 36 0 15.19
app/components/persona/view.rb 100.00 % 20 8 8 0 3.00
app/components/phase_banner/view.rb 100.00 % 32 12 12 0 502.33
app/components/providers/provider_list/view.rb 100.00 % 20 11 11 0 8.18
app/components/tab_navigation/view.rb 100.00 % 12 6 6 0 28.33
app/components/title_bar/view.rb 100.00 % 16 8 8 0 3.75
app/decorators/application_decorator.rb 100.00 % 37 16 16 0 27.31
app/decorators/course_decorator.rb 89.94 % 390 169 152 17 14.46
app/decorators/provider_decorator.rb 0.00 % 15 12 0 12 0.00
app/deserializers/api/v3/deserializable_access_request.rb 0.00 % 12 12 0 12 0.00
app/deserializers/api/v3/deserializable_contact.rb 0.00 % 7 7 0 7 0.00
app/deserializers/api/v3/deserializable_course.rb 100.00 % 56 11 11 0 4.18
app/deserializers/api/v3/deserializable_provider.rb 100.00 % 45 11 11 0 3.27
app/deserializers/api/v3/deserializable_site.rb 100.00 % 15 4 4 0 1.00
app/flows/edit_initial_request_flow.rb 96.00 % 122 50 48 2 11.74
app/flows/initial_request_flow.rb 97.96 % 222 98 96 2 14.41
app/forms/publish/about_your_organisation_form.rb 94.87 % 85 39 37 2 3.18
app/forms/publish/access_request_form.rb 0.00 % 39 31 0 31 0.00
app/forms/publish/age_range_form.rb 100.00 % 111 52 52 0 5.00
app/forms/publish/allocations/edit_initial/number_of_places_form.rb 100.00 % 15 7 7 0 2.00
app/forms/publish/allocations/edit_initial/request_type_form.rb 100.00 % 13 7 7 0 1.00
app/forms/publish/authentication/magic_link_form.rb 100.00 % 26 14 14 0 1.14
app/forms/publish/base_course_form.rb 93.75 % 29 16 15 1 8.56
app/forms/publish/base_model_form.rb 96.15 % 45 26 25 1 35.27
app/forms/publish/base_provider_form.rb 100.00 % 5 3 3 0 4.00
app/forms/publish/course_deletion_form.rb 100.00 % 33 16 16 0 1.31
app/forms/publish/course_fee_form.rb 100.00 % 42 15 15 0 1.60
app/forms/publish/course_information_form.rb 100.00 % 39 20 20 0 3.45
app/forms/publish/course_location_form.rb 100.00 % 60 29 29 0 2.55
app/forms/publish/course_requirement_form.rb 100.00 % 21 10 10 0 1.90
app/forms/publish/course_salary_form.rb 100.00 % 25 12 12 0 1.42
app/forms/publish/course_study_mode_form.rb 100.00 % 19 9 9 0 1.33
app/forms/publish/course_subjects_form.rb 95.65 % 46 23 22 1 3.78
app/forms/publish/course_vacancies_form.rb 100.00 % 127 50 50 0 9.08
app/forms/publish/course_withdrawal_form.rb 100.00 % 39 20 20 0 2.65
app/forms/publish/degree_grade_form.rb 100.00 % 20 10 10 0 1.60
app/forms/publish/degree_start_form.rb 95.45 % 41 22 21 1 2.50
app/forms/publish/funding_type_form_methods.rb 93.75 % 40 16 15 1 4.88
app/forms/publish/gcse_requirements_form.rb 100.00 % 63 28 28 0 4.57
app/forms/publish/initial_request_form.rb 100.00 % 37 18 18 0 6.50
app/forms/publish/interruption/accept_terms_form.rb 100.00 % 25 12 12 0 2.83
app/forms/publish/location_form.rb 91.30 % 46 23 21 2 1.87
app/forms/publish/notification_form.rb 94.44 % 37 18 17 1 1.56
app/forms/publish/provider_contact_form.rb 100.00 % 23 12 12 0 1.25
app/forms/publish/provider_visa_form.rb 100.00 % 19 9 9 0 2.56
app/forms/publish/repeat_request_form.rb 100.00 % 9 5 5 0 2.00
app/forms/publish/subject_requirement_form.rb 100.00 % 36 17 17 0 1.94
app/forms/support/edit_course_form.rb 100.00 % 126 56 56 0 13.93
app/policies/access_request_policy.rb 100.00 % 22 14 14 0 4.00
app/policies/allocation_policy.rb 97.06 % 62 34 33 1 9.06
app/policies/contact_policy.rb 100.00 % 26 14 14 0 2.71
app/policies/course_policy.rb 97.50 % 94 40 39 1 48.65
app/policies/organisation_policy.rb 100.00 % 14 8 8 0 1.38
app/policies/provider_policy.rb 92.16 % 105 51 47 4 48.76
app/policies/recruitment_cycle_policy.rb 100.00 % 30 16 16 0 1.31
app/policies/site_policy.rb 100.00 % 19 11 11 0 2.36
app/policies/site_status_policy.rb 100.00 % 12 7 7 0 1.43
app/policies/user_notification_preferences_policy.rb 100.00 % 22 12 12 0 1.92
app/policies/user_policy.rb 100.00 % 23 15 15 0 2.53
app/serializers/api/public/v1/serializable_course.rb 100.00 % 144 67 67 0 53.03
app/serializers/api/public/v1/serializable_location.rb 100.00 % 55 25 25 0 13.36
app/serializers/api/public/v1/serializable_location_status.rb 100.00 % 22 11 11 0 3.64
app/serializers/api/public/v1/serializable_provider.rb 100.00 % 62 25 25 0 25.28
app/serializers/api/public/v1/serializable_provider_suggestion.rb 100.00 % 20 10 10 0 1.80
app/serializers/api/public/v1/serializable_recruitment_cycle.rb 100.00 % 16 8 8 0 4.75
app/serializers/api/public/v1/serializable_subject.rb 100.00 % 33 17 17 0 56.47
app/serializers/api/public/v1/serializable_subject_area.rb 100.00 % 13 7 7 0 4.00
app/serializers/api/public/v1/serializer_service.rb 100.00 % 25 7 7 0 12.43
app/serializers/api/v3/serializable_course.rb 100.00 % 81 43 43 0 69.37
app/serializers/api/v3/serializable_provider.rb 100.00 % 19 8 8 0 12.63
app/serializers/api/v3/serializable_recruitment_cycle.rb 100.00 % 11 6 6 0 4.00
app/serializers/api/v3/serializable_site.rb 100.00 % 17 8 8 0 6.88
app/serializers/api/v3/serializable_site_status.rb 100.00 % 12 7 7 0 4.00
app/serializers/api/v3/serializable_subject.rb 100.00 % 27 14 14 0 32.29
app/serializers/api/v3/serializable_subject_area.rb 100.00 % 11 7 7 0 4.00
app/serializers/jsonapi_cache_key_helper.rb 100.00 % 5 3 3 0 16.33
app/serializers/jsonapi_course_cache_key_helper.rb 100.00 % 7 3 3 0 22.33
app/services/access_request_approval_service.rb 100.00 % 22 13 13 0 8.62
app/services/allocation_copy_data_service.rb 82.35 % 80 34 28 6 1.32
app/services/allocation_importer_service.rb 95.24 % 46 21 20 1 2.62
app/services/allocation_reporting_service.rb 100.00 % 36 15 15 0 8.40
app/services/allocations/create.rb 100.00 % 52 28 28 0 3.39
app/services/allocations/update.rb 100.00 % 36 19 19 0 6.58
app/services/authentication_service.rb 97.78 % 182 90 88 2 53.24
app/services/course_attribute_formatter_service.rb 100.00 % 67 36 36 0 8.17
app/services/course_reporting_service.rb 100.00 % 97 42 42 0 105.67
app/services/course_search_service.rb 99.10 % 242 111 110 1 37.28
app/services/course_serializers_service.rb 100.00 % 43 15 15 0 58.40
app/services/courses/assign_program_type_service.rb 100.00 % 33 17 17 0 24.76
app/services/courses/assign_subjects_service.rb 100.00 % 54 30 30 0 70.97
app/services/courses/assignable_master_subject_service.rb 100.00 % 24 11 11 0 86.91
app/services/courses/assignable_subject_service.rb 100.00 % 28 13 13 0 1.92
app/services/courses/content_status_service.rb 100.00 % 23 16 16 0 246.31
app/services/courses/copy.rb 100.00 % 63 19 19 0 6.00
app/services/courses/copy_to_provider_service.rb 100.00 % 58 35 35 0 9.71
app/services/courses/creation_service.rb 100.00 % 67 36 36 0 75.78
app/services/courses/fetch.rb 90.91 % 30 11 10 1 36.36
app/services/courses/generate_course_name_service.rb 100.00 % 76 40 40 0 70.53
app/services/courses/validate_custom_age_range_service.rb 100.00 % 33 19 19 0 1304.47
app/services/csv_imports/fake_providers_import.rb 100.00 % 35 17 17 0 4.53
app/services/enrichments/copy_to_course_service.rb 100.00 % 10 7 7 0 3.71
app/services/exports/accredited_course_list.rb 90.00 % 52 20 18 2 1.35
app/services/feature_service.rb 100.00 % 21 10 10 0 7642.90
app/services/generate_and_send_magic_link_service.rb 100.00 % 27 14 14 0 2.50
app/services/geocoder_service.rb 100.00 % 17 10 10 0 3.20
app/services/notification_service/course_published.rb 100.00 % 36 18 18 0 4.17
app/services/notification_service/course_sites_updated.rb 100.00 % 40 17 17 0 3.76
app/services/notification_service/course_subjects_updated.rb 100.00 % 44 20 20 0 4.40
app/services/notification_service/course_updated.rb 100.00 % 46 19 19 0 5.16
app/services/notification_service/course_vacancies_updated.rb 100.00 % 84 40 40 0 6.63
app/services/notification_service/course_withdrawn.rb 100.00 % 36 17 17 0 3.94
app/services/performance_dashboard_service.rb 0.00 % 115 86 0 86 0.00
app/services/provider_enrichments/rollover_enrichment_to_provider_service.rb 0.00 % 10 10 0 10 0.00
app/services/provider_reporting_service.rb 100.00 % 57 23 23 0 57.00
app/services/providers/copy_to_recruitment_cycle_service.rb 100.00 % 72 42 42 0 10.19
app/services/providers/create_fake_provider_service.rb 100.00 % 72 31 31 0 7.84
app/services/providers/generate_course_code_service.rb 100.00 % 17 9 9 0 5.33
app/services/providers/generate_unique_course_code_service.rb 100.00 % 15 8 8 0 6.00
app/services/providers/visa_sponsorship_service.rb 100.00 % 13 7 7 0 3.71
app/services/publish/course_creation_step_service.rb 80.00 % 136 40 32 8 49.05
app/services/publish_reporting_service.rb 100.00 % 127 46 46 0 10.04
app/services/record_first_login_service.rb 100.00 % 7 4 4 0 2.00
app/services/recruitment_cycle_creation_service.rb 90.91 % 18 11 10 1 0.91
app/services/rollover_reporting_service.rb 100.00 % 75 31 31 0 5.71
app/services/rollover_service.rb 100.00 % 70 28 28 0 6.54
app/services/send_welcome_email_service.rb 100.00 % 25 11 11 0 7.00
app/services/serialize_course_service.rb 100.00 % 10 6 6 0 2.00
app/services/service_container.rb 77.27 % 48 22 17 5 1.23
app/services/service_pattern.rb 90.91 % 20 11 10 1 144.45
app/services/sites/code_generator.rb 100.00 % 44 23 23 0 11.30
app/services/sites/copy_to_course_service.rb 100.00 % 15 5 5 0 2.80
app/services/sites/copy_to_provider_service.rb 100.00 % 18 12 12 0 2.92
app/services/statistic_service.rb 100.00 % 15 5 5 0 5.40
app/services/subjects/creator_service.rb 100.00 % 105 24 24 0 830.17
app/services/subjects/financial_incentive_creator_service.rb 100.00 % 80 13 13 0 587.38
app/services/subjects/financial_incentive_set_subject_knowledge_enhancement_course_available_service.rb 100.00 % 20 11 11 0 174.18
app/services/subjects/subject_area_creator_service.rb 100.00 % 15 10 10 0 191.20
app/services/support/data_exports/base.rb 91.67 % 22 12 11 1 1.00
app/services/support/data_exports/data_export.rb 100.00 % 19 8 8 0 1.38
app/services/support/data_exports/users_export.rb 100.00 % 38 14 14 0 1.36
app/services/support/filter.rb 96.88 % 62 32 31 1 20.19
app/services/token/decode_service.rb 100.00 % 54 18 18 0 16.89
app/services/user_associations_service/create.rb 100.00 % 51 27 27 0 3.89
app/services/user_associations_service/delete.rb 100.00 % 38 20 20 0 3.15
app/services/user_sessions/update.rb 100.00 % 32 14 14 0 116.07
app/services/v3/course_search_service.rb 100.00 % 235 112 112 0 41.10
app/services/vacancy_status_determination_service.rb 100.00 % 52 27 27 0 13.48
app/validators/email_address_validator.rb 100.00 % 9 5 5 0 699.00
app/validators/phone_validator.rb 100.00 % 15 8 8 0 869.25
app/validators/postcode_validator.rb 100.00 % 10 6 6 0 417.67
app/validators/reference_number_format_validator.rb 100.00 % 9 5 5 0 3101.40
app/validators/unique_course_validator.rb 100.00 % 25 13 13 0 74.00
app/validators/words_count_validator.rb 100.00 % 16 7 7 0 6142.00
app/view_objects/allocations_view.rb 100.00 % 215 104 104 0 9.33
app/view_objects/notifications_view.rb 100.00 % 36 18 18 0 2.78

app/components/allocation_record_card/view.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 module AllocationRecordCard
  3. 2 class View < ViewComponent::Base
  4. 2 with_collection_parameter :allocation
  5. 2 delegate :provider, :accredited_body, to: :allocation
  6. 2 delegate :provider_name, to: :provider
  7. 2 attr_reader :allocation, :heading_level
  8. 2 def initialize(heading_level = 3, allocation:, allocation_iteration:)
  9. 16 super
  10. 16 @allocation = allocation
  11. 16 @heading_level = heading_level
  12. 16 @iteration = allocation_iteration
  13. end
  14. 2 def provider_code
  15. 16 tag.p("Provider code: #{provider.provider_code}", class: "govuk-caption-m govuk-!-font-size-16 allocation-record-card__id govuk-!-margin-bottom-0")
  16. end
  17. 2 def accredited_body_code
  18. 16 tag.p("Accrediting Body code: #{accredited_body&.provider_code}", class: "govuk-caption-m govuk-!-font-size-16 allocation-record-card__id govuk-!-margin-bottom-0")
  19. end
  20. 2 def accredited_body_name
  21. 16 tag.p("Accredited by #{accredited_body&.provider_name}", class: "govuk-caption-m govuk-!-font-size-16 allocation-record-card__id govuk-!-margin-bottom-0")
  22. end
  23. 2 def number_of_places
  24. 16 tag.p("Allocated places: #{allocation.confirmed_number_of_places}", class: "govuk-heading-m govuk-!-font-size-16 allocation-record-card__id govuk-!-margin-bottom-0")
  25. end
  26. 2 def number_of_uplifts
  27. 16 tag.p("Allocation Uplifts: #{allocation.allocation_uplift&.uplifts.to_i}", class: "govuk-heading-m govuk-!-font-size-16 allocation-record-card__id govuk-!-margin-bottom-0")
  28. end
  29. end
  30. end

app/components/degree_preview_component/view.rb

86.67% lines covered

15 relevant lines. 13 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module DegreePreviewComponent
  3. 1 class View < ViewComponent::Base
  4. 1 include PublishHelper
  5. 1 attr_reader :course
  6. 1 def initialize(course:)
  7. 2 super
  8. 2 @course = course
  9. end
  10. 1 private
  11. 1 def subject_name(course)
  12. 1 case course.subjects.count
  13. when 1
  14. 1 course.subjects.first.subject_name
  15. when 2
  16. "#{course.subjects.first.subject_name} or #{course.subjects.last.subject_name}"
  17. else
  18. course.name
  19. end
  20. end
  21. 1 def degree_grade_content(course)
  22. 2 {
  23. "two_one" => "An undergraduate degree at class 2:1 or above, or equivalent.",
  24. "two_two" => "An undergraduate degree at class 2:2 or above, or equivalent.",
  25. "third_class" => "An undergraduate degree, or equivalent. This should be an honours degree (Third or above), or equivalent.",
  26. "not_required" => "An undergraduate degree, or equivalent.",
  27. }[course.degree_grade]
  28. end
  29. end
  30. end

app/components/degree_row_content_component/view.rb

94.44% lines covered

18 relevant lines. 17 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module DegreeRowContentComponent
  3. 4 class View < ViewComponent::Base
  4. 4 attr_reader :course, :errors
  5. 4 DEGREE_GRADE_MAPPING = {
  6. "two_one" => "2:1 or above, or equivalent",
  7. "two_two" => "2:2 or above, or equivalent",
  8. "third_class" => "Third class degree or above, or equivalent",
  9. "not_required" => "An undergraduate degree, or equivalent",
  10. }.freeze
  11. 4 def initialize(course:, errors: nil)
  12. 35 super
  13. 35 @course = course
  14. 35 @errors = errors
  15. end
  16. 4 def inset_text_css_classes
  17. 4 messages = errors&.values&.flatten
  18. 4 if messages&.include?("Enter degree requirements")
  19. "app-inset-text--narrow-border app-inset-text--error"
  20. else
  21. 4 "app-inset-text--narrow-border app-inset-text--important"
  22. end
  23. end
  24. 4 def has_errors?
  25. 2 inset_text_css_classes.include?("app-inset-text--error")
  26. end
  27. 4 private
  28. 4 def degree_grade_content(course)
  29. 33 DEGREE_GRADE_MAPPING[course.degree_grade]
  30. end
  31. end
  32. end

app/components/filters/allocation_attributes/view.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 3 module Filters
  2. 3 module AllocationAttributes
  3. 3 class View < ViewComponent::Base
  4. 3 attr_accessor :filters
  5. 3 def initialize(filters:)
  6. 9 @filters = filters
  7. 9 super
  8. end
  9. end
  10. end
  11. end

app/components/filters/provider_attributes/view.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 4 module Filters
  2. 4 module ProviderAttributes
  3. 4 class View < ViewComponent::Base
  4. 4 attr_accessor :filters
  5. 4 def initialize(filters:)
  6. 24 @filters = filters
  7. 24 super
  8. end
  9. end
  10. end
  11. end

app/components/filters/user_attributes/view.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 3 module Filters
  2. 3 module UserAttributes
  3. 3 class View < ViewComponent::Base
  4. 3 attr_accessor :filters
  5. 3 def initialize(filters:)
  6. 17 @filters = filters
  7. 17 super
  8. end
  9. 3 private
  10. 3 def checked?(filters, filter, value)
  11. 34 filters && filters[filter]&.include?(value)
  12. end
  13. 3 def label_for(attribute, value)
  14. 34 I18n.t("components.filter.users.#{attribute.pluralize}.#{value}")
  15. end
  16. end
  17. end
  18. end

app/components/filters/view.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module Filters
  3. 4 class View < ViewComponent::Base
  4. 4 attr_accessor :filters, :filter_model
  5. 4 def initialize(filters:, filter_model:)
  6. 44 @filters = filters
  7. 44 @filter_model = filter_model
  8. 44 super
  9. end
  10. 4 private
  11. 4 def tags_for_filter(filter, value)
  12. 17 [value].flatten.map do |v|
  13. 17 { title: title_html(filter, v), remove_link: remove_select_tag_link(filter) }
  14. end
  15. end
  16. 4 def filter_attributes
  17. 44 "::Filters::#{filter_model}Attributes::View".constantize.new(filters: filters)
  18. end
  19. 4 def filter_label(filter)
  20. 17 t("components.filter.#{filter_model.to_s.downcase.pluralize}.#{filter}")
  21. end
  22. 4 def title_html(filter, value)
  23. 17 tag.span("Remove ", class: "govuk-visually-hidden") + value + tag.span(" #{filter.humanize.downcase} filter", class: "govuk-visually-hidden")
  24. end
  25. 4 def remove_select_tag_link(filter)
  26. 46 new_filters = filters.reject { |f| f == filter }
  27. 17 new_filters.to_query.blank? ? nil : "?#{new_filters.to_query}"
  28. end
  29. 4 def reload_path
  30. 16 send("support_#{filter_model.to_s.downcase.pluralize}_path".to_sym)
  31. end
  32. end
  33. end

app/components/flash_banner/view.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module FlashBanner
  3. 4 class View < GovukComponent::Base
  4. 4 attr_reader :flash
  5. 4 FLASH_TYPES = %w[success warning info].freeze
  6. 4 def initialize(flash:)
  7. 998 super(classes: classes, html_attributes: html_attributes)
  8. 998 @flash = flash
  9. end
  10. 4 def display?
  11. 998 flash.any?
  12. end
  13. end
  14. end

app/components/gcse_preview_component/view.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module GcsePreviewComponent
  3. 1 class View < GcseRequirementsComponent::View
  4. 1 include PublishHelper
  5. end
  6. end

app/components/gcse_requirements_component/view.rb

80.0% lines covered

45 relevant lines. 36 lines covered and 9 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module GcseRequirementsComponent
  3. 4 class View < ViewComponent::Base
  4. 4 attr_reader :course, :errors
  5. 4 def initialize(course:, errors: nil)
  6. 42 super
  7. 42 @course = course
  8. 42 @errors = errors
  9. end
  10. 4 def inset_text_css_classes
  11. 40 messages = errors&.values&.flatten
  12. 40 if messages&.include?("Enter GCSE requirements")
  13. 3 "app-inset-text--narrow-border app-inset-text--error"
  14. else
  15. 37 "app-inset-text--narrow-border app-inset-text--important"
  16. end
  17. end
  18. 4 def has_errors?
  19. 19 inset_text_css_classes.include?("app-inset-text--error")
  20. end
  21. 4 private
  22. 4 def required_gcse_summary_content(course)
  23. 19 case course.level
  24. when "primary"
  25. 14 "Grade #{course.gcse_grade_required} (C) or above in English, maths and science, or equivalent qualification"
  26. when "secondary"
  27. 3 "Grade #{course.gcse_grade_required} (C) or above in English and maths, or equivalent qualification"
  28. end
  29. end
  30. 4 def required_gcse_content(course)
  31. case course.level
  32. when "primary"
  33. "GCSE grade #{course.gcse_grade_required} (C) or above in English, maths and science, or equivalent qualification."
  34. when "secondary"
  35. "GCSE grade #{course.gcse_grade_required} (C) or above in English and maths, or equivalent qualification."
  36. end
  37. end
  38. 4 def pending_gcse_summary_content(course)
  39. 19 if course.accept_pending_gcse
  40. 14 "Candidates with pending GCSEs will be considered"
  41. else
  42. 5 "Candidates with pending GCSEs will not be considered"
  43. end
  44. end
  45. 4 def pending_gcse_content(course)
  46. if course.accept_pending_gcse
  47. "We’ll consider candidates who are currently taking GCSEs."
  48. else
  49. "We will not consider candidates with pending GCSEs."
  50. end
  51. end
  52. 4 def gcse_equivalency_summary_content(course)
  53. 19 if course.accept_gcse_equivalency
  54. 11 "Equivalency tests will be accepted in #{equivalencies}."
  55. else
  56. 8 "Equivalency tests will not be accepted"
  57. end
  58. end
  59. 4 def gcse_equivalency_content(course)
  60. if course.accept_gcse_equivalency
  61. "We’ll consider candidates who need to take a GCSE equivalency test in #{equivalencies}."
  62. else
  63. "We will not consider candidates who need to take GCSE equivalency tests."
  64. end
  65. end
  66. 4 def equivalencies
  67. 11 subjects = []
  68. 11 subjects << "English" if course.accept_english_gcse_equivalency.present?
  69. 11 subjects << "maths" if course.accept_maths_gcse_equivalency.present?
  70. 11 subjects << "science" if course.accept_science_gcse_equivalency.present?
  71. 11 subjects.to_sentence(last_word_connector: " or ", two_words_connector: " or ")
  72. end
  73. end
  74. end

app/components/gcse_row_content_component/view.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module GcseRowContentComponent
  3. 4 class View < GcseRequirementsComponent::View
  4. end
  5. end

app/components/header/view.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 class Header::View < GovukComponent::Base
  3. 4 attr_reader :service_name, :current_user
  4. 4 include ActiveModel
  5. 4 def initialize(service_name:, current_user: nil)
  6. 998 super(classes: classes, html_attributes: html_attributes)
  7. 998 @service_name = service_name
  8. 998 @current_user = current_user
  9. end
  10. 4 def environment_header_class
  11. 998 "app-header--#{Settings.environment.name}"
  12. end
  13. end

app/components/navigation_bar/view.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 class NavigationBar::View < GovukComponent::Base
  3. 3 attr_reader :items, :current_path
  4. 3 def initialize(items:, current_path:, current_user: {})
  5. 14 super(classes: classes, html_attributes: html_attributes)
  6. 14 @items = items
  7. 14 @current_path = current_path
  8. 14 @current_user = current_user
  9. end
  10. 3 def item_link(item)
  11. 38 link_params = { class: "moj-primary-navigation__link" }
  12. 38 link_params.merge!(aria: { current: "page" }) if show_current_link?(item)
  13. 38 govuk_link_to(item[:name], item[:url], link_params)
  14. end
  15. 3 def user_signed_in?
  16. 14 @current_user.present?
  17. end
  18. 3 private
  19. 3 def show_current_link?(item)
  20. 78 item.fetch(:current, false) || [item.fetch(:url), item[:additional_url]].compact.any? { |url| current_path.include?(url) }
  21. end
  22. end

app/components/notification_banner/view.rb

100.0% lines covered

36 relevant lines. 36 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module NotificationBanner
  3. 4 class View < GovukComponent::Base
  4. 4 attr_reader :text, :title_id
  5. 4 SUCCESS_TITLE = "Success"
  6. 4 DEFAULT_TITLE = "Important"
  7. 4 SUCCESS_ROLE = "alert"
  8. 4 DEFAULT_ROLE = "region"
  9. 4 DEFAULT_CLASS = "govuk-notification-banner"
  10. 4 def initialize(
  11. title_text: nil,
  12. text: nil,
  13. type: nil,
  14. classes: [],
  15. role: nil,
  16. title_id: nil,
  17. disable_auto_focus: false,
  18. html_attributes: nil
  19. )
  20. 72 super(classes: classes, html_attributes: html_attributes)
  21. 72 @title_text = title_text
  22. 72 @text = text
  23. 72 @type = type
  24. 72 @role = role
  25. 72 @title_id = title_id || "#{DEFAULT_CLASS}-title"
  26. 72 @disable_auto_focus = disable_auto_focus
  27. end
  28. 4 private
  29. 4 attr_reader :title_text, :role, :type, :disable_auto_focus
  30. 4 def default_classes
  31. 72 [DEFAULT_CLASS]
  32. end
  33. 4 def type_class
  34. 72 "#{DEFAULT_CLASS}--#{type}" if success_banner?
  35. end
  36. 4 def title
  37. 72 return title_text if title_text
  38. 71 return SUCCESS_TITLE if success_banner?
  39. 16 DEFAULT_TITLE
  40. end
  41. 4 def role_attribute
  42. 72 return role if role
  43. # If type is success, add `role="alert"` to prioritise the information in
  44. # the notification banner to users of assistive technologies
  45. 71 return SUCCESS_ROLE if success_banner?
  46. # Otherwise add `role="region"` to make the notification banner a landmark
  47. # to help users of assistive technologies to navigate to the banner
  48. 16 DEFAULT_ROLE
  49. end
  50. 4 def data_attributes
  51. 72 attrs = { module: DEFAULT_CLASS }
  52. 72 attrs[:disable_auto_focus] = disable_auto_focus if disable_auto_focus
  53. 72 attrs
  54. end
  55. 4 def success_banner?
  56. 214 type&.to_sym == :success
  57. end
  58. end
  59. end

app/components/page_title/view.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 class PageTitle::View < GovukComponent::Base
  3. 4 I18N_FORMAT = /^\S*\.\S*$/.freeze
  4. 4 attr_accessor :title
  5. 4 def initialize(title: "", has_errors: false)
  6. 371 super(classes: classes, html_attributes: html_attributes)
  7. 371 @title = title
  8. 371 @has_errors = has_errors
  9. end
  10. 4 def build_page_title
  11. 371 [build_error + build_title + build_service_name, "GOV.UK"].join(" - ")
  12. end
  13. 4 private
  14. 4 attr_reader :has_errors
  15. 4 def build_error
  16. 371 has_errors ? "Error: " : ""
  17. end
  18. 4 def build_title
  19. 371 title.match?(I18N_FORMAT) ? I18n.t("components.page_titles.#{title}") : title
  20. end
  21. 4 def build_service_name
  22. 371 title.present? ? " - #{I18n.t('service_name')}" : I18n.t("service_name")
  23. end
  24. end

app/components/paginated_filter/view.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module PaginatedFilter
  3. 4 class View < ViewComponent::Base
  4. 4 attr_reader :filter_params, :collection
  5. 4 def initialize(filter_params:, collection:)
  6. 42 super
  7. 42 @filter_params = filter_params
  8. 42 @collection = collection
  9. end
  10. 4 def filters
  11. 42 filter_params.slice(*allowed_search_params_keys)
  12. end
  13. 4 private
  14. 4 def allowed_search_params_keys
  15. {
  16. 42 User: %i[text_search user_type],
  17. Provider: %i[provider_search course_search],
  18. Allocation: [:text_search],
  19. }[collection.klass.name.to_sym]
  20. end
  21. end
  22. end

app/components/paginator/view.rb

100.0% lines covered

36 relevant lines. 36 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module Paginator
  3. 4 class View < ViewComponent::Base
  4. 4 attr_reader :scope
  5. # This constant limits the number of links to pages rendered by #paginate.
  6. # In some cases, the number of links will be KAMINARI_LINKS_LIMIT + 1 because
  7. # kaminari refuses to leave a gap of just one number e.g. 1 ... 345 ... 9
  8. # so it renders 1 2 3 4 5 ... 9 instead.
  9. 4 KAMINARI_LINKS_LIMIT = 5
  10. 4 def initialize(scope:)
  11. 83 super
  12. 83 @scope = scope
  13. end
  14. 4 def render?
  15. 66 scope.total_pages > 1
  16. end
  17. 4 def page_start
  18. 5 ((scope.current_page - 1) * scope.limit_value) + 1
  19. end
  20. 4 def page_end
  21. 5 [scope.current_page * scope.limit_value, total].min
  22. end
  23. 4 def total
  24. 10 scope.total_count
  25. end
  26. 4 def paginate_configuration
  27. {
  28. 22 window: numbers_each_side,
  29. left: numbers_left,
  30. right: numbers_right,
  31. }
  32. end
  33. 4 private
  34. 4 def total_pages_exceed_limit?
  35. 66 scope.total_pages > KAMINARI_LINKS_LIMIT
  36. end
  37. 4 def numbers_each_side
  38. 22 return 4 unless total_pages_exceed_limit?
  39. 12 if scope.current_page >= (KAMINARI_LINKS_LIMIT - 1) &&
  40. 9 scope.current_page <= scope.total_pages - (KAMINARI_LINKS_LIMIT - 2)
  41. 6 ((KAMINARI_LINKS_LIMIT - 2).to_f / 2).floor
  42. else
  43. 6 0
  44. end
  45. end
  46. 4 def numbers_left
  47. 22 return 0 unless total_pages_exceed_limit?
  48. 12 if scope.current_page < (KAMINARI_LINKS_LIMIT - 1)
  49. 3 KAMINARI_LINKS_LIMIT - 1
  50. else
  51. 9 1
  52. end
  53. end
  54. 4 def numbers_right
  55. 22 return 0 unless total_pages_exceed_limit?
  56. 12 if scope.current_page > scope.total_pages - (KAMINARI_LINKS_LIMIT - 2)
  57. 3 KAMINARI_LINKS_LIMIT - 1
  58. else
  59. 9 1
  60. end
  61. end
  62. end
  63. end

app/components/persona/view.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 module Persona
  3. 2 class View < GovukComponent::Base
  4. 2 attr_accessor :email_address, :first_name, :last_name
  5. 2 def initialize(
  6. email_address:,
  7. first_name:,
  8. last_name:,
  9. classes: [],
  10. html_attributes: nil
  11. )
  12. 4 super(classes: classes, html_attributes: html_attributes)
  13. 4 @email_address = email_address
  14. 4 @first_name = first_name
  15. 4 @last_name = last_name
  16. end
  17. end
  18. end

app/components/phase_banner/view.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 class PhaseBanner::View < ViewComponent::Base
  3. 4 def initialize(no_border: false)
  4. 1001 super
  5. 1001 @no_border = no_border
  6. end
  7. 4 def environment_label
  8. 1001 Settings.environment.label
  9. end
  10. 4 def environment_colour
  11. 1001 {
  12. "development" => "grey",
  13. "qa" => "orange",
  14. "review" => "purple",
  15. "rollover" => "turquoise",
  16. "sandbox" => "purple",
  17. "staging" => "red",
  18. "unknown-environment" => "yellow",
  19. }[Settings.environment.name]
  20. end
  21. 4 def sandbox_mode?
  22. 1001 Settings.environment.name == "sandbox"
  23. end
  24. 4 def feedback_link_to
  25. 999 Settings.feedback.link
  26. end
  27. end

app/components/providers/provider_list/view.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 4 module Providers
  2. 4 module ProviderList
  3. 4 class View < GovukComponent::Base
  4. 4 include Support::TimeHelper
  5. 4 def initialize(provider:)
  6. 17 super(classes: classes, html_attributes: html_attributes)
  7. 17 @provider = provider
  8. end
  9. 4 def formatted_provider_type
  10. 14 Provider.human_attribute_name(@provider.provider_type)
  11. end
  12. 4 def formatted_accrediting_provider
  13. 14 @provider.accredited_body? ? "Yes" : "No"
  14. end
  15. end
  16. end
  17. end

app/components/tab_navigation/view.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module TabNavigation
  3. 4 class View < GovukComponent::Base
  4. 4 attr_reader :items
  5. 4 def initialize(items:)
  6. 77 super(classes: classes, html_attributes: html_attributes)
  7. 77 @items = items
  8. end
  9. end
  10. end

app/components/title_bar/view.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 module TitleBar
  3. 3 class View < ViewComponent::Base
  4. 3 attr_accessor :title
  5. 3 def initialize(title:)
  6. 5 super
  7. 5 @title = title
  8. end
  9. 3 def link
  10. 5 govuk_link_to t("change_organisation"), root_path, class: "title-bar-link inline govuk-link--no-visited-state"
  11. end
  12. end
  13. end

app/controllers/api/public/v1/application_controller.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module Public
  3. 4 module V1
  4. 4 class ApplicationController < PublicAPIController
  5. 4 include PagyPagination
  6. end
  7. end
  8. end
  9. end

app/controllers/api/public/v1/courses_controller.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 module API
  2. 1 module Public
  3. 1 module V1
  4. 1 class CoursesController < API::Public::V1::ApplicationController
  5. 1 def index
  6. 16 render jsonapi: paginate(courses),
  7. include: include_param,
  8. meta: { count: courses.count("course.id") },
  9. class: API::Public::V1::SerializerService.call
  10. rescue ActiveRecord::StatementInvalid
  11. 1 render json: { status: 400, message: "Invalid changed_since value, the format should be an ISO8601 UTC timestamp, for example: `2019-01-01T12:01:00Z`" }.to_json, status: :bad_request
  12. end
  13. 1 private
  14. 1 def courses
  15. 28 @courses ||= CourseSearchService.call(
  16. filter: params[:filter],
  17. sort: params[:sort],
  18. course_scope: recruitment_cycle.courses,
  19. )
  20. end
  21. 1 def recruitment_cycle
  22. 16 @recruitment_cycle = RecruitmentCycle.find_by(
  23. year: params[:recruitment_cycle_year],
  24. ) || RecruitmentCycle.current_recruitment_cycle
  25. end
  26. 1 def include_param
  27. 12 params.fetch(:include, "")
  28. end
  29. end
  30. end
  31. end
  32. end

app/controllers/api/public/v1/provider_suggestions_controller.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 2 module API
  2. 2 module Public
  3. 2 module V1
  4. 2 class ProviderSuggestionsController < API::Public::V1::ApplicationController
  5. 2 def index
  6. 3 return render_json_error(status: 400, message: I18n.t("provider_suggestion.errors.bad_request")) if invalid_query?
  7. 2 found_providers = recruitment_cycle.providers
  8. .with_findable_courses
  9. .provider_search(params[:query])
  10. .limit(10)
  11. 2 render(
  12. jsonapi: found_providers,
  13. class: { Provider: SerializableProviderSuggestion },
  14. )
  15. end
  16. 2 private
  17. 2 def recruitment_cycle
  18. 2 @recruitment_cycle = RecruitmentCycle.find_by(
  19. year: params[:recruitment_cycle_year],
  20. ) || RecruitmentCycle.current_recruitment_cycle
  21. end
  22. 2 def invalid_query?
  23. 3 params[:query].nil? || params[:query].length < 3
  24. end
  25. end
  26. end
  27. end
  28. end

app/controllers/api/public/v1/providers/courses/locations_controller.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. 2 module API
  2. 2 module Public
  3. 2 module V1
  4. 2 module Providers
  5. 2 module Courses
  6. 2 class LocationsController < API::Public::V1::ApplicationController
  7. 2 def index
  8. 5 render jsonapi: locations,
  9. include: include_param,
  10. expose: { course: course, location_statuses: location_statuses },
  11. class: API::Public::V1::SerializerService.call
  12. end
  13. 2 private
  14. 2 def locations
  15. 5 @locations ||= course&.sites
  16. end
  17. 2 def location_statuses
  18. 5 @location_statuses ||= course&.site_statuses
  19. end
  20. 2 def course
  21. 13 @course ||= provider.courses.includes(site_statuses: [:site]).find_by(course_code: params[:course_code])
  22. end
  23. 2 def provider
  24. 4 @provider ||= recruitment_cycle.providers.find_by(provider_code: params[:provider_code])
  25. end
  26. 2 def recruitment_cycle
  27. 4 @recruitment_cycle ||= RecruitmentCycle.find_by(year: params[:recruitment_cycle_year])
  28. end
  29. 2 def include_param
  30. 5 params.fetch(:include, "")
  31. end
  32. end
  33. end
  34. end
  35. end
  36. end
  37. end

app/controllers/api/public/v1/providers/courses_controller.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. 2 module API
  2. 2 module Public
  3. 2 module V1
  4. 2 module Providers
  5. 2 class CoursesController < API::Public::V1::ApplicationController
  6. 2 def index
  7. 12 render jsonapi: paginate(courses),
  8. include: include_param,
  9. class: API::Public::V1::SerializerService.call
  10. end
  11. 2 def show
  12. 5 render jsonapi: course,
  13. include: include_param,
  14. class: API::Public::V1::SerializerService.call
  15. end
  16. 2 private
  17. 2 def courses
  18. 12 @courses ||= CourseSearchService.call(filter: params[:filter],
  19. course_scope: provider.courses)
  20. end
  21. 2 def course
  22. 5 @course ||= provider.courses.find_by!(course_code: params[:code])
  23. end
  24. 2 def provider
  25. 17 @provider ||= recruitment_cycle.providers.find_by!(provider_code: params[:provider_code])
  26. end
  27. 2 def recruitment_cycle
  28. 17 @recruitment_cycle ||= RecruitmentCycle.find_by(year: params[:recruitment_cycle_year])
  29. end
  30. 2 def include_param
  31. 12 params.fetch(:include, "")
  32. end
  33. end
  34. end
  35. end
  36. end
  37. end

app/controllers/api/public/v1/providers/locations_controller.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 2 module API
  2. 2 module Public
  3. 2 module V1
  4. 2 module Providers
  5. 2 class LocationsController < API::Public::V1::ApplicationController
  6. 2 def index
  7. 5 render jsonapi: locations,
  8. include: include_param,
  9. class: API::Public::V1::SerializerService.call
  10. end
  11. 2 private
  12. 2 def locations
  13. 5 @locations ||= provider&.sites
  14. end
  15. 2 def provider
  16. 4 @provider ||= recruitment_cycle.providers.find_by(provider_code: params[:provider_code])
  17. end
  18. 2 def recruitment_cycle
  19. 4 @recruitment_cycle ||= RecruitmentCycle.find_by(year: params[:recruitment_cycle_year])
  20. end
  21. 2 def include_param
  22. 5 params.fetch(:include, "")
  23. end
  24. end
  25. end
  26. end
  27. end
  28. end

app/controllers/api/public/v1/providers_controller.rb

100.0% lines covered

33 relevant lines. 33 lines covered and 0 lines missed.
    
  1. 2 module API
  2. 2 module Public
  3. 2 module V1
  4. 2 class ProvidersController < API::Public::V1::ApplicationController
  5. 2 def index
  6. 19 render jsonapi: paginate(providers),
  7. include: params[:include], class: API::Public::V1::SerializerService.call, fields: fields
  8. end
  9. 2 def show
  10. 8 code = params.fetch(:code, params[:provider_code])
  11. 8 provider = recruitment_cycle.providers
  12. .find_by!(
  13. provider_code: code.upcase,
  14. )
  15. 5 render jsonapi: provider,
  16. class: API::Public::V1::SerializerService.call,
  17. include: params[:include],
  18. fields: fields
  19. end
  20. 2 private
  21. 2 def updated_since
  22. 20 @updated_since ||= params.dig(:filter, :updated_since)
  23. end
  24. 2 def providers
  25. 19 @providers = recruitment_cycle.providers
  26. 19 @providers = if sort_by_provider_ascending?
  27. 18 @providers.by_name_ascending
  28. else
  29. 1 @providers.by_name_descending
  30. end
  31. 19 if updated_since.present?
  32. 1 @providers = @providers.changed_since(updated_since)
  33. end
  34. 19 @providers
  35. end
  36. 2 def recruitment_cycle
  37. 27 @recruitment_cycle = RecruitmentCycle.find_by(
  38. year: params[:recruitment_cycle_year],
  39. ) || RecruitmentCycle.current_recruitment_cycle
  40. end
  41. 2 def fields
  42. 21 { providers: provider_fields } if provider_fields.present?
  43. end
  44. 2 def sort_by_provider_ascending?
  45. 19 sort_field.include?("name") || !sort_by_provider_descending?
  46. end
  47. 2 def sort_by_provider_descending?
  48. 16 sort_field.include?("-name")
  49. end
  50. 2 def sort_field
  51. 35 @sort_field ||= Set.new(params[:sort]&.split(","))
  52. end
  53. 2 def provider_fields
  54. 22 params.dig(:fields, :providers)&.split(",")
  55. end
  56. end
  57. end
  58. end
  59. end

app/controllers/api/public/v1/subject_areas_controller.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 2 module API
  2. 2 module Public
  3. 2 module V1
  4. 2 class SubjectAreasController < API::Public::V1::ApplicationController
  5. 2 def index
  6. 5 render(
  7. jsonapi: SubjectArea.active.includes(subjects: [:financial_incentive]),
  8. include: params[:include],
  9. class: API::Public::V1::SerializerService.call,
  10. )
  11. end
  12. end
  13. end
  14. end
  15. end

app/controllers/api/public/v1/subjects_controller.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 1 module API
  2. 1 module Public
  3. 1 module V1
  4. 1 class SubjectsController < API::Public::V1::ApplicationController
  5. 1 def index
  6. 3 subjects = Subject.active.includes(:financial_incentive)
  7. 3 if params["sort"] == "name"
  8. 2 subjects = subjects.order(:subject_name)
  9. end
  10. 3 render jsonapi: subjects,
  11. class: API::Public::V1::SerializerService.call,
  12. include: params[:include],
  13. fields: fields
  14. end
  15. 1 def fields
  16. 3 { subjects: subject_fields } if subject_fields.present?
  17. end
  18. 1 def subject_fields
  19. 4 params.dig(:fields, :subjects)&.split(",")
  20. end
  21. end
  22. end
  23. end
  24. end

app/controllers/api/system/application_controller.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 module API
  2. 1 module System
  3. 1 class ApplicationController < ::ApplicationController
  4. 1 before_action -> { skip_authorization }
  5. 1 def authenticate
  6. 2 authenticate_or_request_with_http_token do |token|
  7. 2 ActiveSupport::SecurityUtils.secure_compare(
  8. token,
  9. Settings.system_authentication_token,
  10. )
  11. end
  12. end
  13. end
  14. end
  15. end

app/controllers/api/v3/accredited_provider_training_providers_controller.rb

85.71% lines covered

42 relevant lines. 36 lines covered and 6 lines missed.
    
  1. 2 module API
  2. 2 module V3
  3. 2 class AccreditedProviderTrainingProvidersController < API::V3::ApplicationController
  4. 2 before_action :build_recruitment_cycle
  5. 2 before_action :build_provider
  6. 2 class TrainingProviderSearch
  7. 2 attr_reader :provider, :params
  8. 2 def initialize(provider:, params:)
  9. 30 @provider = provider
  10. 30 @params = params
  11. end
  12. 2 def call
  13. 30 scope = all_providers.order(:provider_name)
  14. 30 if params[:filter]
  15. 16 scope = scope.where(id: eligible_training_provider_ids)
  16. end
  17. 30 scope
  18. end
  19. 2 private
  20. 2 def course_scope
  21. 16 provider_courses = Course.where(provider: [provider.id])
  22. 16 training_provider_courses = Course.where(provider: training_providers).where(accredited_body_code: provider.provider_code)
  23. 16 provider_courses.or(training_provider_courses).findable
  24. end
  25. 2 def eligible_training_provider_ids
  26. 16 CourseSearchService.call(filter: params[:filter], course_scope: course_scope)
  27. .pluck(:provider_id)
  28. end
  29. 2 def all_providers
  30. 30 provider.training_providers.or(Provider.where(id: provider.id))
  31. end
  32. 2 def training_providers
  33. 16 provider.training_providers
  34. end
  35. end
  36. 2 def index
  37. authorize @provider, :can_list_training_providers?
  38. providers = TrainingProviderSearch.new(provider: @provider, params: params)
  39. .call
  40. .include_accredited_courses_counts(@provider.provider_code)
  41. accredited_courses_counts = {}
  42. providers.each do |p|
  43. accredited_courses_counts[p.provider_code] = p.accredited_courses_count
  44. end
  45. render jsonapi: providers,
  46. include: params[:include],
  47. meta: {
  48. accredited_courses_counts: accredited_courses_counts,
  49. }
  50. end
  51. 2 def show
  52. 1 training_provider = @provider.training_providers.find_by(provider_code: params[:training_provider_code])
  53. 1 authorize training_provider, :can_show_training_provider?
  54. 1 render jsonapi: training_provider
  55. end
  56. 2 private
  57. 2 def build_recruitment_cycle
  58. 1 @recruitment_cycle = RecruitmentCycle.find_by(
  59. year: params[:recruitment_cycle_year],
  60. ) || RecruitmentCycle.current_recruitment_cycle
  61. end
  62. 2 def build_provider
  63. 1 @provider = @recruitment_cycle.providers.find_by!(
  64. provider_code: params[:provider_code].upcase,
  65. )
  66. end
  67. end
  68. end
  69. end

app/controllers/api/v3/application_controller.rb

88.24% lines covered

34 relevant lines. 30 lines covered and 4 lines missed.
    
  1. 4 module API
  2. 4 module V3
  3. 4 class ApplicationController < PublicAPIController
  4. 4 attr_reader :current_user
  5. 4 rescue_from ActiveRecord::RecordNotFound, with: :jsonapi_404
  6. 4 before_action :store_request_id
  7. 4 def authenticate
  8. authenticate_or_request_with_http_token do |token|
  9. @current_user = AuthenticationService.new(logger: Rails.logger).execute(token)
  10. assign_sentry_contexts
  11. @current_user.present?
  12. end
  13. end
  14. 4 def jsonapi_404
  15. 3 render jsonapi: nil, status: :not_found
  16. end
  17. 4 private
  18. 4 def paginate(scope)
  19. 54 _pagy, paginated_records = pagy(scope, items: per_page, page: page)
  20. 53 paginated_records
  21. end
  22. 4 def per_page
  23. 54 params[:page] ||= {}
  24. 54 [(params.dig(:page, :per_page) || default_per_page).to_i, max_per_page].min
  25. end
  26. 4 def default_per_page
  27. 50 100
  28. end
  29. 4 def max_per_page
  30. 46 100
  31. end
  32. 4 def page
  33. 54 params[:page] ||= {}
  34. 54 (params.dig(:page, :page) || 1).to_i
  35. end
  36. 4 def build_recruitment_cycle
  37. 89 @recruitment_cycle = RecruitmentCycle.find_by(
  38. year: params[:recruitment_cycle_year],
  39. ) || RecruitmentCycle.current_recruitment_cycle
  40. end
  41. 4 def fields_param
  42. 59 params.fetch(:fields, {})
  43. .permit(:subject_areas, :subjects, :courses, :providers, :site_statuses)
  44. .to_h
  45. 6 .map { |k, v| [k, v.split(",").map(&:to_sym)] }
  46. end
  47. 4 def store_request_id
  48. 99 RequestStore.store[:request_id] = request.uuid
  49. end
  50. end
  51. end
  52. end

app/controllers/api/v3/courses_controller.rb

100.0% lines covered

28 relevant lines. 28 lines covered and 0 lines missed.
    
  1. 2 module API
  2. 2 module V3
  3. 2 class CoursesController < API::V3::ApplicationController
  4. 2 before_action :build_recruitment_cycle
  5. 2 before_action :build_provider
  6. 2 before_action :build_courses
  7. 2 def index
  8. 51 course_search = ::V3::CourseSearchService.call(filter: params[:filter], sort: params[:sort], course_scope: @courses)
  9. 51 render jsonapi: paginate(course_search),
  10. fields: fields_param,
  11. include: params[:include],
  12. meta: { count: course_search.size },
  13. class: CourseSerializersService.new(provider_serializer: API::V3::SerializableProvider).execute,
  14. cache: Rails.cache
  15. end
  16. 2 def show
  17. 5 @course = @courses.find_by!(course_code: params[:code].upcase)
  18. 5 if @course.is_published?
  19. # https://github.com/jsonapi-rb/jsonapi-rails/issues/113
  20. 3 render jsonapi: @course,
  21. fields: fields_param,
  22. include: params[:include],
  23. class: CourseSerializersService.new.execute
  24. else
  25. 2 raise ActiveRecord::RecordNotFound
  26. end
  27. end
  28. 2 private
  29. 2 def max_per_page
  30. 48 if fields_for_sitemap?
  31. 2 20_000
  32. else
  33. 46 super
  34. end
  35. end
  36. 2 def fields_for_sitemap?
  37. 48 if (courses = params.dig(:fields, :courses))
  38. 2 (courses.split(",") & %w[course_code provider_code changed_at]).size == 3
  39. end
  40. end
  41. 2 def build_courses
  42. 56 courses_base = @provider.present? ? @provider.courses : @recruitment_cycle.courses
  43. 56 @courses = courses_base.includes(
  44. :enrichments,
  45. subjects: [:financial_incentive],
  46. site_statuses: [:site],
  47. provider: %i[recruitment_cycle ucas_preferences],
  48. ).findable
  49. end
  50. 2 def build_provider
  51. 57 if params[:provider_code].present?
  52. 11 @provider = @recruitment_cycle.providers.find_by!(
  53. provider_code: params[:provider_code].upcase,
  54. )
  55. end
  56. end
  57. end
  58. end
  59. end

app/controllers/api/v3/provider_suggestions_controller.rb

90.91% lines covered

11 relevant lines. 10 lines covered and 1 lines missed.
    
  1. 1 module API
  2. 1 module V3
  3. 1 class ProviderSuggestionsController < API::V3::ApplicationController
  4. 1 before_action :build_recruitment_cycle
  5. 1 def index
  6. 7 return render(status: :bad_request) if params[:query].nil? || params[:query].length < 3
  7. 5 found_providers = @recruitment_cycle.providers
  8. .with_findable_courses
  9. .provider_search(params[:query])
  10. .limit(10)
  11. 5 render(
  12. jsonapi: found_providers,
  13. class: { Provider: SerializableProvider },
  14. )
  15. end
  16. 1 private
  17. 1 def begins_with_alphanumeric(string)
  18. string.match?(/^[[:alnum:]].*$/)
  19. end
  20. end
  21. end
  22. end

app/controllers/api/v3/providers_controller.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. 2 module API
  2. 2 module V3
  3. 2 class ProvidersController < API::V3::ApplicationController
  4. 2 before_action :build_recruitment_cycle
  5. 2 def index
  6. 16 if params[:search].present? && (params[:search].length < 2)
  7. 1 return render(status: :bad_request)
  8. end
  9. 15 build_fields_for_index
  10. 15 @providers = @recruitment_cycle.providers.includes(:recruitment_cycle)
  11. 15 @providers = @providers.provider_search(params[:search]) if params[:search].present?
  12. 15 render jsonapi: @providers.by_name_ascending, class: { Provider: API::V3::SerializableProvider }, fields: @fields
  13. end
  14. 2 def show
  15. 9 code = params.fetch(:code, params[:provider_code])
  16. 9 @provider = @recruitment_cycle.providers
  17. .includes(:sites, :courses, courses: [:enrichments, :sites, { site_statuses: [:site], provider: [:recruitment_cycle], subjects: [:financial_incentive] }])
  18. .find_by!(
  19. provider_code: code.upcase,
  20. )
  21. 9 render jsonapi: @provider,
  22. class: CourseSerializersService.new.execute,
  23. include: params[:include],
  24. fields: { providers: %i[provider_code provider_name courses
  25. recruitment_cycle_year address1 address2
  26. address3 address4 postcode region_code
  27. email website telephone train_with_us
  28. train_with_disability sites
  29. accredited_bodies accredited_body?
  30. can_sponsor_student_visa
  31. can_sponsor_skilled_worker_visa] }
  32. end
  33. 2 private
  34. 2 def build_fields_for_index
  35. 15 @fields = default_fields_for_index
  36. 15 return if params[:fields].blank? || params[:fields][:providers].blank?
  37. 1 @fields[:providers] = params[:fields][:providers].split(",")
  38. end
  39. 2 def default_fields_for_index
  40. {
  41. 15 providers: %w[provider_name provider_code recruitment_cycle_year],
  42. }
  43. end
  44. end
  45. end
  46. end

app/controllers/api/v3/subject_areas_controller.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 module API
  2. 1 module V3
  3. 1 class SubjectAreasController < API::V3::ApplicationController
  4. 1 def index
  5. 3 render jsonapi: SubjectArea.active.includes(subjects: [:financial_incentive]), fields: fields_param, include: params[:include], class: CourseSerializersService.new.execute
  6. end
  7. end
  8. end
  9. end

app/controllers/api/v3/subjects_controller.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 module API
  2. 1 module V3
  3. 1 class SubjectsController < API::V3::ApplicationController
  4. 1 def index
  5. 3 subjects = Subject.active
  6. 3 if params["sort"] == "subject_name"
  7. 1 subjects = subjects.order(:subject_name)
  8. end
  9. 3 render jsonapi: subjects, fields: fields_param, include: params[:include], class: CourseSerializersService.new.execute
  10. end
  11. end
  12. end
  13. end

app/controllers/api_controller.rb

0.0% lines covered

7 relevant lines. 0 lines covered and 7 lines missed.
    
  1. class APIController < ActionController::API
  2. include EmitsRequestEvents
  3. include ActionController::HttpAuthentication::Token::ControllerMethods
  4. include Pundit::Authorization
  5. # child must define authenticate method
  6. before_action :authenticate
  7. after_action :verify_authorized
  8. end

app/controllers/api_error_controller.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. 1 class APIErrorController < ActionController::API
  2. 1 def error500
  3. 1 raise "This is an error that is triggered by the application when a user accesses the /error_500 route. If you are seeing this in logs, you can ignore it."
  4. end
  5. 1 def error_nodb
  6. raise PG::ConnectionBad, "This is an error that is triggered by the application when a user accesses the /error_nodb route. If you are seeing this in logs, you can ignore it."
  7. end
  8. end

app/controllers/application_controller.rb

80.0% lines covered

15 relevant lines. 12 lines covered and 3 lines missed.
    
  1. 4 class ApplicationController < ActionController::Base
  2. 4 include EmitsRequestEvents
  3. 4 include Authentication
  4. 4 helper_method :current_user
  5. 4 before_action :authenticate
  6. 4 include Pundit::Authorization
  7. 4 include Pagy::Backend
  8. 1508 before_action :enforce_basic_auth, if: -> { BasicAuthenticable.required? }
  9. 4 default_form_builder GOVUKDesignSystemFormBuilder::FormBuilder
  10. 4 private
  11. 4 def enforce_basic_auth
  12. authenticate_or_request_with_http_basic do |username, password|
  13. BasicAuthenticable.authenticate(username, password)
  14. end
  15. end
  16. 4 def render_not_found
  17. render "errors/not_found", status: :not_found, formats: :html
  18. end
  19. end

app/controllers/component_previews_controller.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class ComponentPreviewsController < ViewComponentsController
  2. 1 include Authentication
  3. 1 helper_method :current_user
  4. end

app/controllers/concerns/authentication.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 4 module Authentication
  2. 4 def user_session
  3. 2298 @user_session ||= UserSession.load_from_session(session)
  4. end
  5. 4 def current_user
  6. 8723 @current_user ||= User.find_by(email: user_session&.email)
  7. end
  8. 4 def authenticated?
  9. 1022 current_user.present?
  10. end
  11. 4 def authenticate
  12. 1022 if !authenticated?
  13. 5 session["post_dfe_sign_in_path"] = request.fullpath
  14. 5 redirect_to sign_in_path
  15. end
  16. end
  17. end

app/controllers/concerns/basic_authenticable.rb

64.29% lines covered

14 relevant lines. 9 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. # Enforces HTTP Basic Auth
  3. 4 module BasicAuthenticable
  4. 4 class << self
  5. 4 def required?
  6. 1504 Settings.basic_auth.enabled
  7. end
  8. 4 def authenticate(username, password)
  9. validate(username, password)
  10. end
  11. 4 def validate(username, password)
  12. utils.secure_compare(::Digest::SHA256.hexdigest(auth_username), ::Digest::SHA256.hexdigest(username)) &
  13. utils.secure_compare(::Digest::SHA256.hexdigest(auth_password), ::Digest::SHA256.hexdigest(password))
  14. end
  15. 4 def auth_username
  16. @auth_username ||= Settings.basic_auth.username
  17. end
  18. 4 def auth_password
  19. @auth_password ||= Settings.basic_auth.password
  20. end
  21. 4 def utils
  22. ActiveSupport::SecurityUtils
  23. end
  24. end
  25. end

app/controllers/concerns/copy_course_content.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. 4 module CopyCourseContent
  2. 4 extend ActiveSupport::Concern
  3. 4 private
  4. 4 def copy_content_check(fields)
  5. 22 fetch_course_list_to_copy_from
  6. 22 if params[:copy_from].present?
  7. 4 fetch_course_to_copy_from
  8. 4 @copied_fields = ::Courses::Copy.get_present_fields_in_source_course(fields, @source_course, @course)
  9. end
  10. end
  11. 4 def copy_boolean_check(fields)
  12. 15 fetch_course_list_to_copy_from
  13. 15 if params[:copy_from].present?
  14. 4 fetch_course_to_copy_from
  15. 4 @copied_fields = ::Courses::Copy.get_boolean_fields(fields, @source_course, @course)
  16. end
  17. end
  18. 4 def fetch_course_to_copy_from
  19. 8 @source_course = ::Courses::Fetch.by_code(
  20. provider_code: params[:provider_code],
  21. course_code: params[:copy_from],
  22. )
  23. end
  24. 4 def fetch_course_list_to_copy_from
  25. 37 @courses_by_accrediting_provider = ::Courses::Fetch.by_accrediting_provider(@provider)
  26. 37 @self_accredited_courses = @courses_by_accrediting_provider.delete(@provider.provider_name)
  27. 41 @courses_by_accrediting_provider&.transform_values! { |v| v.reject { |x| x.id == course.id } }
  28. 112 @self_accredited_courses = @self_accredited_courses&.reject { |c| c.id == course.id }
  29. end
  30. end

app/controllers/concerns/emits_request_events.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 4 module EmitsRequestEvents
  2. 4 extend ActiveSupport::Concern
  3. 4 include ApplicationHelper
  4. 4 included do
  5. 8 after_action :trigger_request_event
  6. end
  7. 4 def trigger_request_event
  8. 1648 if FeatureService.enabled?(:send_request_data_to_bigquery)
  9. 2 request_event = BigQuery::RequestEvent.new do |event|
  10. 2 event.with_request_details(request)
  11. 2 event.with_response_details(response)
  12. 2 if respond_to?(:current_user, true)
  13. 1 event.with_user(current_user)
  14. end
  15. end
  16. 2 SendEventToBigQueryJob.perform_later(request_event.as_json)
  17. end
  18. end
  19. end

app/controllers/concerns/error_handlers/base.rb

93.75% lines covered

16 relevant lines. 15 lines covered and 1 lines missed.
    
  1. 4 module ErrorHandlers
  2. 4 module Base
  3. 4 extend ActiveSupport::Concern
  4. 4 included do
  5. 5 rescue_from(StandardError) do |e|
  6. 3 raise e unless Settings.render_json_errors
  7. 2 Sentry.capture_exception(e)
  8. 2 render_json_error(status: 500)
  9. end
  10. 10 rescue_from(ActiveRecord::RecordNotFound) { render_json_error(status: 404) }
  11. end
  12. 4 private
  13. 4 def render_json_error(status:, resource: nil, message: nil)
  14. 18 if resource.nil?
  15. 18 render json: error_hash(status, message), status: status
  16. else
  17. render jsonapi_errors: resource.errors, status: status
  18. end
  19. end
  20. 4 def error_hash(status, message = nil)
  21. {
  22. 18 errors: [
  23. {
  24. status: status,
  25. title: I18n.t("errors.#{status}.title"),
  26. detail: I18n.t("errors.#{status}.detail", message: message),
  27. },
  28. ],
  29. }
  30. end
  31. end
  32. end

app/controllers/concerns/error_handlers/pagy.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 4 module ErrorHandlers
  2. 4 module Pagy
  3. 4 def self.included(base)
  4. 4 base.include(ErrorHandlers::Base)
  5. 4 base.class_eval do
  6. 4 rescue_from ::Pagy::OverflowError do |_exception|
  7. 10 render_json_error(status: 400, message: I18n.t("pagy.overflow"))
  8. end
  9. end
  10. end
  11. end
  12. end

app/controllers/concerns/pagy_pagination.rb

100.0% lines covered

30 relevant lines. 30 lines covered and 0 lines missed.
    
  1. 4 module PagyPagination
  2. 4 extend ActiveSupport::Concern
  3. 4 def jsonapi_links
  4. 65 pagy_results.present? ? pagination_links : {}
  5. end
  6. 4 def paginate(scope)
  7. 47 @pagy_results ||= pagy(scope, items: per_page, page: page)
  8. 37 pagy_results.second
  9. end
  10. 4 private
  11. 4 attr_accessor :pagy_results
  12. 4 def pagination_links
  13. 37 meta = pagy_metadata(pagy_results.first, absolute: true)
  14. {
  15. 37 first: meta[:first_url],
  16. last: meta[:last_url],
  17. 37 prev: meta[:prev].nil? ? nil : meta[:prev_url],
  18. 37 next: meta[:next].nil? ? nil : meta[:next_url],
  19. }
  20. end
  21. 4 def per_page
  22. 47 [(per_page_parameter || default_per_page).to_i, max_per_page].min
  23. end
  24. 4 def default_per_page
  25. 29 100
  26. end
  27. 4 def max_per_page
  28. 47 500
  29. end
  30. 4 def page
  31. 47 (page_parameter || 1).to_i
  32. end
  33. 4 def page_parameter
  34. 47 return params[:page][:page] if page_is_nested?
  35. 47 params[:page]
  36. end
  37. 4 def per_page_parameter
  38. 47 return params[:page][:per_page] if page_is_nested?
  39. 47 params[:per_page]
  40. end
  41. 4 def page_is_nested?
  42. 94 params[:page].is_a?(ActionController::Parameters)
  43. end
  44. end

app/controllers/concerns/study_mode_vacancy_mapper.rb

37.5% lines covered

8 relevant lines. 3 lines covered and 5 lines missed.
    
  1. 4 module StudyModeVacancyMapper
  2. 4 extend ActiveSupport::Concern
  3. 4 def update_vac_status(study_mode, site_status)
  4. case study_mode
  5. when "full_time"
  6. site_status.update(vac_status: :full_time_vacancies)
  7. when "part_time"
  8. site_status.update(vac_status: :part_time_vacancies)
  9. when "full_time_or_part_time"
  10. site_status.update(vac_status: :both_full_time_and_part_time_vacancies)
  11. else
  12. raise "Unexpected study mode #{study_mode}"
  13. end
  14. end
  15. end

app/controllers/concerns/success_message.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 4 module SuccessMessage
  2. 4 extend ActiveSupport::Concern
  3. 4 def course_details_success_message(value)
  4. 6 raise TypeError unless value.is_a?(String)
  5. 6 flash[:success] = @course.is_published? ? I18n.t("success.value_published", value: value) : I18n.t("success.value_saved", value: value)
  6. end
  7. 4 def course_description_success_message(value)
  8. 7 raise TypeError unless value.is_a?(String)
  9. 7 flash[:success] = @course.only_published? ? I18n.t("success.value_published", value: value) : I18n.t("success.value_saved", value: value)
  10. end
  11. end

app/controllers/concerns/validate_jsonapi_type.rb

0.0% lines covered

9 relevant lines. 0 lines covered and 9 lines missed.
    
  1. module ValidateJsonapiType
  2. extend ActiveSupport::Concern
  3. def validate_jsonapi_type(params, type)
  4. # jsonapi-rb doesn't appear to validate the type of data record that's
  5. # been given us, so even though we say "require(:session)" it won't
  6. # validate the type field of the data object that was passed through.
  7. #
  8. # So for now, we do this by hand.
  9. sent_type = params[:_jsonapi][:data][:type]
  10. unless sent_type == type
  11. raise ActionController::BadRequest, "data type '#{sent_type}' did not match expected type '#{type}'"
  12. end
  13. end
  14. end

app/controllers/errors_controller.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. class ErrorsController < ApplicationController
  2. skip_before_action :authenticate
  3. def not_found
  4. render status: 404
  5. end
  6. def forbidden
  7. render status: 403
  8. end
  9. def internal_server_error
  10. render status: 500
  11. end
  12. end

app/controllers/heartbeat_controller.rb

91.67% lines covered

24 relevant lines. 22 lines covered and 2 lines missed.
    
  1. 1 require "sidekiq/api"
  2. 1 class HeartbeatController < ActionController::API
  3. 1 def ping
  4. 1 render body: "PONG"
  5. end
  6. 1 def healthcheck
  7. checks = {
  8. 9 database: database_alive?,
  9. redis: redis_alive?,
  10. sidekiq_processes: sidekiq_processes_checks,
  11. }
  12. 9 status = checks.values.all? ? :ok : :service_unavailable
  13. 9 render status: status,
  14. json: {
  15. checks: checks,
  16. }
  17. end
  18. 1 def sha
  19. 1 render json: { sha: ENV.fetch("COMMIT_SHA", nil) }
  20. end
  21. 1 private
  22. 1 def database_alive?
  23. 9 ActiveRecord::Base.connection.active?
  24. rescue PG::ConnectionBad
  25. false
  26. end
  27. 1 def redis_alive?
  28. 9 Sidekiq.redis_info
  29. 7 true
  30. rescue StandardError
  31. 2 false
  32. end
  33. 1 def sidekiq_processes_checks
  34. 9 stats = Sidekiq::Stats.new
  35. 9 processes = Sidekiq::ProcessSet.new
  36. # Iterate over each Sidekiq queue and ensure that there is a process
  37. # running for it.
  38. 9 stats.queues.keys.all? do |queue|
  39. 18 processes.any? { |process| queue.in? process["queues"] }
  40. end
  41. rescue StandardError
  42. false
  43. end
  44. end

app/controllers/magic_link_sessions_controller.rb

100.0% lines covered

34 relevant lines. 34 lines covered and 0 lines missed.
    
  1. 1 if AuthenticationService.magic_link?
  2. 1 class MagicLinkSessionsController < ApplicationController
  3. 1 layout "application"
  4. 1 skip_before_action :authenticate
  5. 1 before_action :redirect_if_token_is_invalid
  6. 1 before_action :redirect_if_token_expired
  7. 1 def create
  8. 1 update_user
  9. 1 set_user_session
  10. 1 record_first_login
  11. 1 send_welcome_email
  12. 1 redirect_to root_path
  13. end
  14. 1 private
  15. 1 def magic_link_params
  16. 3 params.permit(:email, :token)
  17. end
  18. 1 def redirect_if_token_is_invalid
  19. 3 return unless params[:token] != user.magic_link_token
  20. 1 redirect_to root_path, flash: {
  21. error: {
  22. id: "publish-authentication-magic-link-form-email-field",
  23. message: t("publish_authentication.magic_link.invalid_token"),
  24. },
  25. }
  26. end
  27. 1 def redirect_if_token_expired
  28. 2 return unless magic_link_token_expired?
  29. 1 redirect_to root_path, flash: {
  30. error: {
  31. id: "publish-authentication-magic-link-form-email-field",
  32. message: t("publish_authentication.magic_link.expired"),
  33. },
  34. }
  35. end
  36. 1 def user
  37. 11 @user ||= User.find_by(email: magic_link_params[:email])
  38. end
  39. 1 def magic_link_token_expired?
  40. 2 magic_link_token_age = Time.zone.now - user.magic_link_token_sent_at
  41. 2 magic_link_token_age > Settings.magic_link.max_token_age.seconds
  42. end
  43. 1 def update_user
  44. 1 user.update!(
  45. last_login_date_utc: Time.now.utc,
  46. magic_link_token: nil,
  47. magic_link_token_sent_at: nil,
  48. )
  49. end
  50. 1 def set_user_session
  51. 1 session["user"] = {
  52. "email" => user.email,
  53. "first_name" => user.first_name,
  54. "last_name" => user.last_name,
  55. "last_active_at" => Time.zone.now,
  56. }
  57. end
  58. 1 def record_first_login
  59. 1 RecordFirstLoginService.new.execute(current_user: user)
  60. end
  61. 1 def send_welcome_email
  62. 1 SendWelcomeEmailService.call(current_user: user)
  63. end
  64. end
  65. end

app/controllers/magic_links_controller.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 1 if AuthenticationService.magic_link?
  2. 1 class MagicLinksController < ApplicationController
  3. 1 layout "application"
  4. 1 skip_before_action :authenticate
  5. 1 def new
  6. 4 @magic_link_form = Publish::Authentication::MagicLinkForm.new
  7. end
  8. 1 def create
  9. 2 @magic_link_form = Publish::Authentication::MagicLinkForm.new(email: magic_link_params[:email])
  10. 2 if @magic_link_form.submit
  11. 1 redirect_to magic_link_sent_path
  12. else
  13. 1 render :new
  14. end
  15. end
  16. 1 def magic_link_sent; end
  17. 1 private
  18. 1 def magic_link_params
  19. 2 params.require(:publish_authentication_magic_link_form).permit(:email)
  20. end
  21. end
  22. end

app/controllers/pages_controller.rb

88.89% lines covered

9 relevant lines. 8 lines covered and 1 lines missed.
    
  1. 1 class PagesController < ApplicationController
  2. 1 skip_before_action :authenticate, only: %i[
  3. accessibility
  4. cookies
  5. guidance
  6. performance_dashboard
  7. privacy
  8. terms
  9. ]
  10. 1 def accessibility; end
  11. 1 def cookies; end
  12. 1 def guidance; end
  13. 1 def performance_dashboard
  14. @performance_data = PerformanceDashboardService.call
  15. end
  16. 1 def privacy; end
  17. 1 def terms; end
  18. end

app/controllers/personas_controller.rb

0.0% lines covered

7 relevant lines. 0 lines covered and 7 lines missed.
    
  1. if AuthenticationService.persona?
  2. class PersonasController < ApplicationController
  3. layout "application"
  4. skip_before_action :authenticate
  5. def index; end
  6. end
  7. end

app/controllers/public_api_controller.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 4 class PublicAPIController < ActionController::API
  2. 4 include EmitsRequestEvents
  3. 4 include Pagy::Backend
  4. 4 include ErrorHandlers::Pagy
  5. 4 include Pundit::Authorization
  6. end

app/controllers/publish/access_requests_controller.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class AccessRequestsController < PublishController
  3. 6 before_action -> { authorize(access_request) }, except: :index
  4. 1 def index
  5. 4 authorize(AccessRequest)
  6. 4 @access_requests = AccessRequest.requested.includes(:requester)
  7. end
  8. 1 def approve
  9. 1 access_request.approved!
  10. 1 flash[:success] = "Successfully approved request"
  11. 1 redirect_to inform_publisher_publish_access_request_path
  12. end
  13. 1 def inform_publisher; end
  14. 1 def confirm; end
  15. 1 def destroy
  16. 1 access_request.destroy
  17. 1 flash[:success] = "Successfully deleted the Access Request"
  18. 1 redirect_to publish_access_requests_path
  19. end
  20. 1 private
  21. 1 def access_request
  22. 7 @access_request ||= AccessRequest.includes(:requester, requester: [:organisations]).find(params[:id])
  23. end
  24. end
  25. end

app/controllers/publish/courses/accredited_body_controller.rb

56.16% lines covered

73 relevant lines. 41 lines covered and 32 lines missed.
    
  1. 2 module Publish
  2. 2 module Courses
  3. 2 class AccreditedBodyController < PublishController
  4. 2 before_action :build_course_params, only: :continue
  5. 2 include CourseBasicDetailConcern
  6. 2 def edit
  7. build_provider
  8. end
  9. 2 def continue
  10. 2 authorize(@provider, :can_create_course?)
  11. 2 code = course_params[:accredited_body_code]
  12. 2 query = @accredited_body
  13. 2 @errors = errors_for_search_query(code, query)
  14. 2 if @errors.present?
  15. 1 render :new
  16. 1 elsif other_selected_with_no_autocompleted_code?(code)
  17. redirect_to(
  18. search_new_publish_provider_recruitment_cycle_courses_accredited_body_path(
  19. query: @accredited_body,
  20. course: course_params,
  21. ),
  22. )
  23. else
  24. 1 params[:course][:accredited_body_code] = @autocompleted_provider_code if @autocompleted_provider_code.present?
  25. 1 super
  26. end
  27. end
  28. 2 def search_new
  29. authorize(provider, :can_create_course?)
  30. # These are not before_action hooks as they conflict with hooks
  31. # defined within the CourseBasicDetailConcern and cannot be overridden
  32. # without causing failures in other routes in this controller
  33. build_new_course
  34. build_provider
  35. build_previous_course_creation_params
  36. @query = params[:query]
  37. @provider_suggestions = recruitment_cycle.providers.with_findable_courses.provider_search(@query).limit(10)
  38. end
  39. 2 def update
  40. build_provider
  41. code = update_course_params[:accredited_body_code]
  42. query = update_course_params[:accredited_body]
  43. @errors = errors_for_search_query(code, query)
  44. return render :edit if @errors.present?
  45. if update_params[:accredited_body_code] == "other"
  46. redirect_to_provider_search
  47. elsif @course.update(update_params)
  48. redirect_to_update_successful
  49. else
  50. @errors = @course.errors.messages
  51. render :edit
  52. end
  53. end
  54. 2 def search
  55. build_course
  56. @query = params[:query]
  57. @provider_suggestions = recruitment_cycle.providers.with_findable_courses.provider_search(@query).limit(10)
  58. end
  59. 2 private
  60. 2 def build_provider
  61. @provider = RecruitmentCycle.find_by(year: params[:recruitment_cycle_year])
  62. .providers
  63. .find_by(provider_code: params[:provider_code])
  64. end
  65. 2 def error_keys
  66. 6 [:accredited_body_code]
  67. end
  68. 2 def redirect_to_provider_search
  69. redirect_to(
  70. accredited_body_search_provider_recruitment_cycle_course_path(
  71. @course.provider_code,
  72. @course.recruitment_cycle_year,
  73. @course.course_code,
  74. query: update_course_params[:accredited_body],
  75. ),
  76. )
  77. end
  78. 2 def redirect_to_update_successful
  79. flash[:success] = I18n.t("success.saved")
  80. redirect_to(
  81. details_provider_recruitment_cycle_course_path(
  82. @course.provider_code,
  83. @course.recruitment_cycle_year,
  84. @course.course_code,
  85. ),
  86. )
  87. end
  88. 2 def current_step
  89. 6 :accredited_body
  90. end
  91. 2 def build_course_params
  92. 2 @accredited_body = params[:course].delete(:accredited_body)
  93. 2 @autocompleted_provider_code = params[:course].delete(:autocompleted_provider_code)
  94. end
  95. 2 def errors_for_search_query(code, query)
  96. 2 errors = {}
  97. 2 if other_selected_with_no_autocompleted_code?(code) && query.length < 2
  98. errors = { accredited_body: ["Accredited body search too short, enter 2 or more characters"] }
  99. 2 elsif code.blank?
  100. 1 errors = { accredited_body_code: ["Pick an accredited body"] }
  101. end
  102. 2 errors
  103. end
  104. 2 def build_course
  105. @course = Course
  106. .where(recruitment_cycle_year: params[:recruitment_cycle_year])
  107. .where(provider_code: params[:provider_code])
  108. .includes(:accrediting_provider)
  109. .find(params[:code])
  110. .first
  111. end
  112. 2 def update_course_params
  113. params.require(:course).permit(
  114. :autocompleted_provider_code,
  115. :accredited_body_code,
  116. :accredited_body,
  117. )
  118. end
  119. 2 def update_params
  120. autocompleted_code = update_course_params[:autocompleted_provider_code]
  121. code = update_course_params[:accredited_body_code]
  122. {
  123. accredited_body_code: autocompleted_code.presence || code,
  124. }
  125. end
  126. 2 def other_selected_with_no_autocompleted_code?(code)
  127. 3 code == "other" && @autocompleted_provider_code.blank?
  128. end
  129. end
  130. end
  131. end

app/controllers/publish/courses/age_range_controller.rb

97.73% lines covered

44 relevant lines. 43 lines covered and 1 lines missed.
    
  1. 3 module Publish
  2. 3 module Courses
  3. 3 class AgeRangeController < PublishController
  4. 3 include CourseBasicDetailConcern
  5. 3 decorates_assigned :course
  6. 3 def edit
  7. 3 if params[:display_errors] == "true"
  8. form_object.valid?
  9. end
  10. 3 render locals: { form_object: form_object }
  11. end
  12. 3 def update
  13. 3 if form_object.valid?
  14. 2 course_details_success_message("age range")
  15. 2 update_age_range_param
  16. 2 if @course.update(course_params)
  17. 2 redirect_to(
  18. details_publish_provider_recruitment_cycle_course_path(
  19. @course.provider_code,
  20. @course.recruitment_cycle_year,
  21. @course.course_code,
  22. ),
  23. )
  24. end
  25. else
  26. 1 render :edit, locals: { form_object: form_object }
  27. end
  28. end
  29. 3 private
  30. 3 def form_object
  31. 7 @form_object ||= AgeRangeForm.new(@course, params: permitted_params)
  32. end
  33. 3 def permitted_params
  34. 6 if params[:course]
  35. 3 (params[:course] || ActionController::Parameters.new).permit(:age_range_in_years, :course_age_range_in_years_other_from, :course_age_range_in_years_other_to)
  36. else
  37. 99 @course.attributes.select { |k, _v| %w[age_range_in_years course_age_range_in_years_other_from course_age_range_in_years_other_to].include?(k) }
  38. end
  39. end
  40. 3 def error_keys
  41. 29 [:age_range_in_years]
  42. end
  43. 3 def update_age_range_param
  44. 2 params[:course][:age_range_in_years] = "#{age_from_param}_to_#{age_to_param}" if valid_custom_age_range?
  45. end
  46. 3 def valid_custom_age_range?
  47. 2 age_from_param.present? && age_to_param.present? && age_range_is_other?
  48. end
  49. 3 def age_to_param
  50. 3 course_param[:course_age_range_in_years_other_to]
  51. end
  52. 3 def age_from_param
  53. 4 course_param[:course_age_range_in_years_other_from]
  54. end
  55. 3 def age_range_param
  56. 1 course_param[:age_range_in_years]
  57. end
  58. 3 def age_range_is_other?
  59. 1 age_range_param == "other"
  60. end
  61. 3 def course_param
  62. 8 params[:course]
  63. end
  64. 3 def current_step
  65. 15 :age_range
  66. end
  67. 3 def build_course
  68. 6 super
  69. 6 authorize @course
  70. end
  71. end
  72. end
  73. end

app/controllers/publish/courses/applications_open_controller.rb

92.59% lines covered

27 relevant lines. 25 lines covered and 2 lines missed.
    
  1. 2 module Publish
  2. 2 module Courses
  3. 2 class ApplicationsOpenController < PublishController
  4. 2 before_action :build_recruitment_cycle
  5. 2 before_action :build_course_params, only: %i[update continue]
  6. 2 include CourseBasicDetailConcern
  7. 2 def update
  8. super
  9. end
  10. 2 def continue
  11. 4 super
  12. end
  13. 2 private
  14. 2 def actual_params
  15. 10 params.require(:course)
  16. .except(
  17. :qualification,
  18. :maths,
  19. :english,
  20. :science,
  21. :funding_type,
  22. :level,
  23. :is_send,
  24. :study_mode,
  25. :age_range_in_years,
  26. :sites_ids,
  27. :subjects_ids,
  28. :goto_confirmation,
  29. :accredited_body_code,
  30. )
  31. .permit(
  32. :start_date,
  33. :applications_open_from,
  34. :day,
  35. :month,
  36. :year,
  37. )
  38. end
  39. # This is needed to handle the fact that dates are optionally specified as year/month/day in the UI
  40. # This method assigns the params to the correct YYYY-MM-DD value given what is selected
  41. 2 def build_course_params
  42. 4 if params.key?(:course)
  43. applications_open_from =
  44. 4 if actual_params["applications_open_from"] == "other"
  45. 1 "#{actual_params['year']}-#{actual_params['month']}-#{actual_params['day']}"
  46. else
  47. 3 actual_params["applications_open_from"]
  48. end
  49. 4 params["course"]["applications_open_from"] = applications_open_from
  50. else
  51. ActionController::Parameters.new({}).permit
  52. end
  53. end
  54. 2 def build_recruitment_cycle
  55. 9 cycle_year = params.fetch(
  56. :recruitment_cycle_year,
  57. Settings.current_recruitment_cycle_year,
  58. )
  59. 9 @recruitment_cycle = RecruitmentCycle.find_by(year: cycle_year)
  60. end
  61. 2 def current_step
  62. 12 :applications_open
  63. end
  64. 2 def error_keys
  65. 17 [:applications_open_from]
  66. end
  67. end
  68. end
  69. end

app/controllers/publish/courses/apprenticeship_controller.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 2 module Publish
  2. 2 module Courses
  3. 2 class ApprenticeshipController < PublishController
  4. 2 include CourseBasicDetailConcern
  5. 2 private
  6. 2 def current_step
  7. 11 :apprenticeship
  8. end
  9. 2 def error_keys
  10. 26 %i[funding_type program_type]
  11. end
  12. end
  13. end
  14. end

app/controllers/publish/courses/base_funding_type_controller.rb

0.0% lines covered

32 relevant lines. 0 lines covered and 32 lines missed.
    
  1. module Publish
  2. module Courses
  3. class BaseFundingTypeController < PublishController
  4. include SuccessMessage
  5. private
  6. def course
  7. @course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
  8. end
  9. def course_enrichment
  10. @course_enrichment ||= course.enrichments.find_or_initialize_draft
  11. end
  12. def funding_type_params
  13. params.require(funding_type).permit(*funding_type_fields)
  14. end
  15. def formatted_params
  16. if funding_type_params[:course_length] == "Other" && funding_type_params[:course_length_other_length].present?
  17. funding_type_params.merge(
  18. course_length: funding_type_params[:course_length_other_length],
  19. )
  20. else
  21. funding_type_params
  22. end
  23. end
  24. def funding_type
  25. raise NotImplementedError
  26. end
  27. def funding_type_fields
  28. raise NotImplementedError
  29. end
  30. end
  31. end
  32. end

app/controllers/publish/courses/course_information_controller.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. 3 module Publish
  2. 3 module Courses
  3. 3 class CourseInformationController < PublishController
  4. 3 include CopyCourseContent
  5. 3 def edit
  6. 14 authorize(provider)
  7. 14 @course_information_form = CourseInformationForm.new(course_enrichment)
  8. 14 copy_content_check(::Courses::Copy::ABOUT_FIELDS)
  9. 14 @course_information_form.valid? if show_errors_on_publish?
  10. end
  11. 3 def update
  12. 3 authorize(provider)
  13. 3 @course_information_form = CourseInformationForm.new(course_enrichment, params: course_information_params)
  14. 3 if @course_information_form.save!
  15. 2 course_description_success_message("course information")
  16. 2 redirect_to publish_provider_recruitment_cycle_course_path(
  17. provider.provider_code,
  18. recruitment_cycle.year,
  19. course.course_code,
  20. )
  21. else
  22. 1 render :edit
  23. end
  24. end
  25. 3 private
  26. 3 def course
  27. 45 @course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
  28. end
  29. 3 def course_information_params
  30. 3 params
  31. .require(:publish_course_information_form)
  32. .permit(
  33. CourseInformationForm::FIELDS,
  34. )
  35. end
  36. 3 def course_enrichment
  37. 17 @course_enrichment ||= course.enrichments.find_or_initialize_draft
  38. end
  39. end
  40. end
  41. end

app/controllers/publish/courses/degrees/grade_controller.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 module Courses
  3. 1 module Degrees
  4. 1 class GradeController < PublishController
  5. 1 def edit
  6. 4 authorize(provider)
  7. 4 @grade_form = DegreeGradeForm.build_from_course(course)
  8. end
  9. 1 def update
  10. 3 authorize(provider)
  11. 3 @grade_form = DegreeGradeForm.new(grade: grade_params)
  12. 3 if course.is_primary? && @grade_form.save(course)
  13. 1 course_description_success_message("minimum degree classification")
  14. 1 redirect_to publish_provider_recruitment_cycle_course_path
  15. 2 elsif @grade_form.save(course)
  16. 1 redirect_to degrees_subject_requirements_publish_provider_recruitment_cycle_course_path
  17. else
  18. 1 @errors = @grade_form.errors.messages
  19. 1 render :edit
  20. end
  21. end
  22. 1 private
  23. 1 def course
  24. 10 @course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
  25. end
  26. 1 def grade_params
  27. 3 return if params[:publish_degree_grade_form].blank?
  28. 2 params.require(:publish_degree_grade_form).permit(:grade)[:grade]
  29. end
  30. end
  31. end
  32. end
  33. end

app/controllers/publish/courses/degrees/start_controller.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 module Courses
  3. 1 module Degrees
  4. 1 class StartController < PublishController
  5. 1 def edit
  6. 6 authorize(provider)
  7. 6 @start_form = DegreeStartForm.new
  8. 6 @start_form.build_from_course(course)
  9. 6 @start_form.valid? if show_errors_on_publish?
  10. end
  11. 1 def update
  12. 5 authorize(provider)
  13. 5 @start_form = DegreeStartForm.new(degree_grade_required: grade_required_params)
  14. 5 if course.is_primary? && @start_form.save(course)
  15. 1 flash[:success] = I18n.t("success.value_saved", value: "minimum degree classification")
  16. 1 redirect_to publish_provider_recruitment_cycle_course_path
  17. 4 elsif @start_form.save(course)
  18. 1 redirect_to degrees_subject_requirements_publish_provider_recruitment_cycle_course_path
  19. 3 elsif @start_form.degree_grade_required.present?
  20. 2 redirect_to degrees_grade_publish_provider_recruitment_cycle_course_path
  21. else
  22. 1 @errors = @start_form.errors.messages
  23. 1 render :edit
  24. end
  25. end
  26. 1 private
  27. 1 def course
  28. 17 @course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
  29. end
  30. 1 def grade_required_params
  31. 5 return if params[:publish_degree_start_form].blank?
  32. 4 params.require(:publish_degree_start_form).permit(:degree_grade_required)[:degree_grade_required]
  33. end
  34. end
  35. end
  36. end
  37. end

app/controllers/publish/courses/degrees/subject_requirements_controller.rb

100.0% lines covered

32 relevant lines. 32 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 module Courses
  3. 1 module Degrees
  4. 1 class SubjectRequirementsController < PublishController
  5. 1 include CopyCourseContent
  6. 1 decorates_assigned :source_course
  7. 1 before_action :redirect_to_course_details_page_if_course_is_primary
  8. 1 def edit
  9. 8 authorize(provider)
  10. 8 set_backlink
  11. 8 @subject_requirements_form = SubjectRequirementForm.build_from_course(course)
  12. 8 copy_boolean_check(::Courses::Copy::SUBJECT_REQUIREMENTS_FIELDS)
  13. end
  14. 1 def update
  15. 3 authorize(provider)
  16. 3 @subject_requirements_form = SubjectRequirementForm.new(subject_requirements_params)
  17. 3 if @subject_requirements_form.save(@course)
  18. 2 course_description_success_message("degree requirements")
  19. 2 redirect_to publish_provider_recruitment_cycle_course_path
  20. else
  21. 1 set_backlink
  22. 1 @errors = @subject_requirements_form.errors.messages
  23. 1 render :edit
  24. end
  25. end
  26. 1 private
  27. 1 def course
  28. 44 @course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
  29. end
  30. 1 def subject_requirements_params
  31. 3 params
  32. .require(:publish_subject_requirement_form)
  33. .permit(:additional_degree_subject_requirements, :degree_subject_requirements)
  34. end
  35. 1 def set_backlink
  36. 9 @backlink = if course.degree_grade == "not_required"
  37. 1 degrees_start_publish_provider_recruitment_cycle_course_path
  38. else
  39. 8 degrees_grade_publish_provider_recruitment_cycle_course_path
  40. end
  41. end
  42. 1 def redirect_to_course_details_page_if_course_is_primary
  43. 11 redirect_to publish_provider_recruitment_cycle_course_path if course.is_primary?
  44. end
  45. end
  46. end
  47. end
  48. end

app/controllers/publish/courses/deletions_controller.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 module Courses
  3. 1 class DeletionsController < PublishController
  4. 6 before_action :redirect_to_courses, if: -> { course.is_published? }
  5. 1 def edit
  6. 2 authorize(provider)
  7. 2 @course_deletion_form = CourseDeletionForm.new(course)
  8. end
  9. 1 def destroy
  10. 2 authorize(provider)
  11. 2 @course_deletion_form = CourseDeletionForm.new(course, params: deletion_params)
  12. 2 if @course_deletion_form.destroy!
  13. 1 flash[:success] = "#{@course.name} (#{@course.course_code}) has been deleted"
  14. 1 redirect_to publish_provider_recruitment_cycle_courses_path(
  15. provider.provider_code,
  16. recruitment_cycle.year,
  17. )
  18. else
  19. 1 render :edit
  20. end
  21. end
  22. 1 private
  23. 1 def redirect_to_courses
  24. 1 redirect_to publish_provider_recruitment_cycle_courses_path(
  25. provider.provider_code,
  26. course.recruitment_cycle_year,
  27. )
  28. end
  29. 1 def course
  30. 10 @course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
  31. end
  32. 1 def deletion_params
  33. 2 params.require(:publish_course_deletion_form).permit(CourseDeletionForm::FIELDS)
  34. end
  35. end
  36. end
  37. end

app/controllers/publish/courses/fee_or_salary_controller.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 module Courses
  3. 1 class FeeOrSalaryController < PublishController
  4. 1 include CourseBasicDetailConcern
  5. 1 private
  6. 1 def current_step
  7. 14 :fee_or_salary
  8. end
  9. 1 def error_keys
  10. 29 %i[funding_type program_type]
  11. end
  12. end
  13. end
  14. end

app/controllers/publish/courses/fees_controller.rb

0.0% lines covered

35 relevant lines. 0 lines covered and 35 lines missed.
    
  1. module Publish
  2. module Courses
  3. class FeesController < BaseFundingTypeController
  4. include CopyCourseContent
  5. decorates_assigned :source_course
  6. def edit
  7. authorize(provider)
  8. @course_fee_form = CourseFeeForm.new(course_enrichment)
  9. copy_content_check(::Courses::Copy::FEES_FIELDS)
  10. @course_fee_form.valid? if show_errors_on_publish?
  11. end
  12. def update
  13. authorize(provider)
  14. @course_fee_form = CourseFeeForm.new(course_enrichment, params: formatted_params)
  15. if @course_fee_form.save!
  16. course_description_success_message("course length and fees")
  17. redirect_to publish_provider_recruitment_cycle_course_path(
  18. provider.provider_code,
  19. recruitment_cycle.year,
  20. course.course_code,
  21. )
  22. else
  23. render :edit
  24. end
  25. end
  26. private
  27. def funding_type
  28. :publish_course_fee_form
  29. end
  30. def funding_type_fields
  31. CourseFeeForm::FIELDS
  32. end
  33. end
  34. end
  35. end

app/controllers/publish/courses/gcse_requirements_controller.rb

100.0% lines covered

33 relevant lines. 33 lines covered and 0 lines missed.
    
  1. 2 module Publish
  2. 2 module Courses
  3. 2 class GcseRequirementsController < PublishController
  4. 2 include CopyCourseContent
  5. 2 decorates_assigned :source_course
  6. 2 def edit
  7. 7 authorize(provider)
  8. 7 @gcse_requirements_form = GcseRequirementsForm.build_from_course(course)
  9. 7 copy_boolean_check(::Courses::Copy::GCSE_FIELDS)
  10. 7 @gcse_requirements_form.valid? if show_errors_on_publish?
  11. end
  12. 2 def update
  13. 3 authorize(provider)
  14. 3 @gcse_requirements_form = GcseRequirementsForm.new(**gcse_requirements_form_params.merge(level: course.level))
  15. 3 if @gcse_requirements_form.save(course)
  16. 1 course_description_success_message("GCSE requirements")
  17. 1 redirect_to publish_provider_recruitment_cycle_course_path
  18. else
  19. 2 @errors = @gcse_requirements_form.errors.messages
  20. 2 render :edit
  21. end
  22. end
  23. 2 private
  24. 2 def course
  25. 32 @course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
  26. end
  27. 2 def translatable_params
  28. 3 %i[accept_pending_gcse
  29. accept_gcse_equivalency
  30. accept_english_gcse_equivalency
  31. accept_maths_gcse_equivalency
  32. accept_science_gcse_equivalency]
  33. end
  34. 2 def gcse_requirements_form_params
  35. 3 translatable_params.index_with do |key|
  36. 15 translate_params(publish_gcse_requirements_form_params[key])
  37. end.merge(additional_gcse_equivalencies: helpers.raw(publish_gcse_requirements_form_params[:additional_gcse_equivalencies]))
  38. end
  39. 2 def publish_gcse_requirements_form_params
  40. 18 @publish_gcse_requirements_form_params ||= params.require(:publish_gcse_requirements_form).permit(
  41. :accept_pending_gcse,
  42. :accept_english_gcse_equivalency,
  43. :accept_gcse_equivalency,
  44. { accept_english_gcse_equivalency: [] },
  45. { accept_maths_gcse_equivalency: [] },
  46. { accept_science_gcse_equivalency: [] },
  47. :additional_gcse_equivalencies,
  48. )
  49. end
  50. 2 def translate_params(value)
  51. 15 return if value.blank?
  52. 6 if value.is_a?(Array)
  53. 2 (%w[Maths English Science] & value).present?
  54. else
  55. 4 value == "true"
  56. end
  57. end
  58. end
  59. end
  60. end

app/controllers/publish/courses/level_controller.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 3 module Publish
  2. 3 module Courses
  3. 3 class LevelController < PublishController
  4. 3 include CourseBasicDetailConcern
  5. 3 private
  6. 3 def error_keys
  7. 56 [:level]
  8. end
  9. 3 def current_step
  10. 18 :level
  11. end
  12. end
  13. end
  14. end

app/controllers/publish/courses/modern_languages_controller.rb

93.33% lines covered

60 relevant lines. 56 lines covered and 4 lines missed.
    
  1. 4 module Publish
  2. 4 module Courses
  3. 4 class ModernLanguagesController < PublishController
  4. 4 decorates_assigned :course
  5. 4 before_action :build_course, only: %i[edit update]
  6. 4 before_action :build_course_params, only: [:continue]
  7. 4 include CourseBasicDetailConcern
  8. 4 def new
  9. 7 authorize(@provider, :can_create_course?)
  10. 7 return if has_modern_languages_subject?
  11. 4 redirect_to next_step
  12. end
  13. 4 def continue
  14. 2 super
  15. end
  16. 4 def edit
  17. 4 authorize(provider)
  18. 4 return if selected_non_language_subjects_ids.include? modern_languages_subject_id.to_s
  19. 1 redirect_to(
  20. details_publish_provider_recruitment_cycle_course_path(
  21. @course.provider_code,
  22. @course.recruitment_cycle_year,
  23. @course.course_code,
  24. ),
  25. )
  26. end
  27. 4 def update
  28. 2 authorize(provider)
  29. 2 if course_subjects_form.save!
  30. 1 flash[:success] = I18n.t("success.saved")
  31. 1 redirect_to(
  32. details_publish_provider_recruitment_cycle_course_path(
  33. @course.provider_code,
  34. @course.recruitment_cycle_year,
  35. @course.course_code,
  36. ),
  37. )
  38. else
  39. 1 @errors = @course.errors.messages
  40. 1 render :edit
  41. end
  42. end
  43. 4 def back
  44. authorize(@provider, :edit?)
  45. if has_modern_languages_subject?
  46. redirect_to new_publish_provider_recruitment_cycle_courses_modern_languages_path(path_params)
  47. else
  48. redirect_to @back_link_path
  49. end
  50. end
  51. 4 def current_step
  52. 14 :modern_languages
  53. end
  54. 4 private
  55. 4 def updated_subject_list
  56. 2 @updated_subject_list ||= selected_language_subjects_ids.concat(selected_non_language_subjects_ids)
  57. end
  58. 4 def course_subjects_form
  59. 2 @course_subjects_form ||= CourseSubjectsForm.new(@course, params: updated_subject_list)
  60. end
  61. 4 def error_keys
  62. 17 [:modern_languages_subjects]
  63. end
  64. 4 def modern_languages_subject_id
  65. 10 @modern_languages_subject_id ||= @course.edit_course_options[:modern_languages_subject].id
  66. end
  67. 4 def selected_subjects(param_key)
  68. 8 edit_course_options_key = param_key == :language_ids ? :modern_languages : :subjects
  69. 8 ids = params.dig(:course, param_key)&.map(&:to_i) || []
  70. 8 @course.edit_course_options[edit_course_options_key].filter_map do |subject|
  71. 192 subject.id.to_s if ids.include?(subject.id)
  72. end
  73. end
  74. 4 def selected_language_subjects_ids
  75. 2 selected_subjects(:language_ids)
  76. end
  77. 4 def selected_non_language_subjects_ids
  78. 6 selected_subjects(:subjects_ids)
  79. end
  80. 4 def has_modern_languages_subject?
  81. 13 @course.subjects.any? { |subject| subject.id == modern_languages_subject_id }
  82. end
  83. 4 def build_course_params
  84. 2 build_new_course # to get languages edit_options
  85. 2 params[:course][:subjects_ids] = selected_non_language_subject_ids
  86. 2 params[:course][:subjects_ids] += params[:course][:language_ids] if params[:course][:language_ids]
  87. 2 params[:course].delete(:language_ids)
  88. end
  89. 4 def non_language_subject_ids
  90. 2 @course.edit_course_options[:subjects].map(&:id).map(&:to_s)
  91. end
  92. 4 def selected_non_language_subject_ids
  93. 2 non_language_subject_ids & params[:course][:subjects_ids]
  94. end
  95. end
  96. end
  97. end

app/controllers/publish/courses/outcome_controller.rb

89.47% lines covered

38 relevant lines. 34 lines covered and 4 lines missed.
    
  1. 3 module Publish
  2. 3 module Courses
  3. 3 class OutcomeController < PublishController
  4. 3 include CourseBasicDetailConcern
  5. 3 before_action :order_edit_options, only: %i[edit new]
  6. 3 def edit
  7. 3 super
  8. end
  9. 3 def update
  10. 1 authorize(provider)
  11. 1 @errors = errors
  12. 1 return render :edit if @errors.present?
  13. 1 if @course.update(course_params)
  14. 1 course_details_success_message("course outcome")
  15. 1 redirect_to(
  16. details_publish_provider_recruitment_cycle_course_path(
  17. @course.provider_code,
  18. @course.recruitment_cycle_year,
  19. @course.course_code,
  20. ),
  21. )
  22. else
  23. @errors = @course.errors.messages
  24. render :edit
  25. end
  26. end
  27. 3 def new
  28. 8 super
  29. end
  30. 3 private
  31. 3 def order_edit_options
  32. 11 qualification_options = @course.edit_course_options[:qualifications]
  33. 11 @course.edit_course_options[:qualifications] = if @course.level == "further_education"
  34. 2 non_qts_qualifications(qualification_options)
  35. else
  36. 9 qts_qualifications(qualification_options)
  37. end
  38. end
  39. 3 def current_step
  40. 17 :outcome
  41. end
  42. 3 def qts_qualifications(edit_options)
  43. 9 options = %w[pgce_with_qts qts pgde_with_qts]
  44. 9 if edit_options.sort != options.sort
  45. raise "Non QTS qualification options do not match"
  46. end
  47. 9 options
  48. end
  49. 3 def non_qts_qualifications(edit_options)
  50. 2 options = %w[pgce pgde]
  51. 2 if edit_options.sort != options.sort
  52. raise "QTS qualification options do not match"
  53. end
  54. 2 options
  55. end
  56. 3 def errors
  57. 6 params.dig(:course, :qualification) ? {} : { qualification: ["Pick an outcome"] }
  58. end
  59. end
  60. end
  61. end

app/controllers/publish/courses/requirements_controller.rb

96.0% lines covered

25 relevant lines. 24 lines covered and 1 lines missed.
    
  1. 1 module Publish
  2. 1 module Courses
  3. 1 class RequirementsController < PublishController
  4. 1 include CopyCourseContent
  5. 1 decorates_assigned :source_course
  6. 1 def edit
  7. 8 authorize(provider)
  8. 8 @course_requirement_form = CourseRequirementForm.new(course_enrichment)
  9. 8 if @course.recruitment_cycle_year.to_i >= Provider::CHANGES_INTRODUCED_IN_2022_CYCLE
  10. 8 copy_content_check(::Courses::Copy::POST_2022_CYCLE_REQUIREMENTS_FIELDS)
  11. else
  12. copy_content_check(::Courses::Copy::PRE_2022_CYCLE_REQUIREMENTS_FIELDS)
  13. end
  14. end
  15. 1 def update
  16. 2 authorize(provider)
  17. 2 @course_requirement_form = CourseRequirementForm.new(course_enrichment, params: course_requirement_params)
  18. 2 if @course_requirement_form.save!
  19. 1 flash[:success] = I18n.t("success.saved")
  20. 1 redirect_to publish_provider_recruitment_cycle_course_path(
  21. provider.provider_code,
  22. recruitment_cycle.year,
  23. course.course_code,
  24. )
  25. else
  26. 1 render :edit
  27. end
  28. end
  29. 1 private
  30. 1 def course
  31. 27 @course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
  32. end
  33. 1 def course_requirement_params
  34. 2 params
  35. .require(:publish_course_requirement_form)
  36. .permit(
  37. CourseRequirementForm::FIELDS,
  38. )
  39. end
  40. 1 def course_enrichment
  41. 10 @course_enrichment ||= course.enrichments.find_or_initialize_draft
  42. end
  43. end
  44. end
  45. end

app/controllers/publish/courses/salary_controller.rb

0.0% lines covered

32 relevant lines. 0 lines covered and 32 lines missed.
    
  1. module Publish
  2. module Courses
  3. class SalaryController < BaseFundingTypeController
  4. def edit
  5. authorize(provider)
  6. @course_salary_form = CourseSalaryForm.new(course_enrichment)
  7. @course_salary_form.valid? if show_errors_on_publish?
  8. end
  9. def update
  10. authorize(provider)
  11. @course_salary_form = CourseSalaryForm.new(course_enrichment, params: formatted_params)
  12. if @course_salary_form.save!
  13. flash[:success] = I18n.t("success.saved")
  14. redirect_to publish_provider_recruitment_cycle_course_path(
  15. provider.provider_code,
  16. recruitment_cycle.year,
  17. course.course_code,
  18. )
  19. else
  20. render :edit
  21. end
  22. end
  23. private
  24. def funding_type
  25. :publish_course_salary_form
  26. end
  27. def funding_type_fields
  28. CourseSalaryForm::FIELDS
  29. end
  30. end
  31. end
  32. end

app/controllers/publish/courses/sites_controller.rb

82.61% lines covered

46 relevant lines. 38 lines covered and 8 lines missed.
    
  1. 3 module Publish
  2. 3 module Courses
  3. 3 class SitesController < PublishController
  4. 3 before_action :build_course_params, only: %i[continue]
  5. 3 include CourseBasicDetailConcern
  6. 3 def continue
  7. 3 super
  8. end
  9. 3 def new
  10. 6 authorize(@provider, :edit?)
  11. 6 if @provider.sites.count == 1
  12. set_default_site
  13. redirect_to next_step
  14. end
  15. end
  16. 3 def edit
  17. 2 authorize(provider)
  18. 2 @course_location_form = CourseLocationForm.new(@course)
  19. end
  20. 3 def update
  21. 2 authorize(provider)
  22. 2 @course_location_form = CourseLocationForm.new(@course, params: location_params)
  23. 2 if @course_location_form.save!
  24. 1 course_details_success_message("course locations")
  25. 1 redirect_to details_publish_provider_recruitment_cycle_course_path(
  26. provider.provider_code,
  27. recruitment_cycle.year,
  28. course.course_code,
  29. )
  30. else
  31. 1 render :edit
  32. end
  33. end
  34. 3 def back
  35. authorize(@provider, :edit?)
  36. if @provider.sites.count > 1
  37. redirect_to new_publish_provider_recruitment_cycle_courses_locations_path(path_params)
  38. else
  39. redirect_to @back_link_path
  40. end
  41. end
  42. 3 private
  43. 3 def current_step
  44. 11 :location
  45. end
  46. 3 def error_keys
  47. 11 [:sites]
  48. end
  49. 3 def set_default_site
  50. params["course"] ||= {}
  51. params["course"]["sites_ids"] = [@provider.sites.first.id]
  52. end
  53. 3 def build_course_params
  54. 3 selected_site_ids = params.dig(:course, :site_statuses_attributes)
  55. .values
  56. 8 .select { |field| field["selected"] == "1" }
  57. 4 .map { |field| field["id"] }
  58. 3 params["course"]["sites_ids"] = selected_site_ids
  59. 3 params["course"].delete("site_statuses_attributes")
  60. end
  61. 3 def location_params
  62. 2 return { site_ids: nil } if params[:publish_course_location_form][:site_ids].all?(&:empty?)
  63. 1 params.require(:publish_course_location_form).permit(site_ids: [])
  64. end
  65. 3 def build_course
  66. 4 @course = provider.courses.find_by!(course_code: params[:code])
  67. end
  68. end
  69. end
  70. end

app/controllers/publish/courses/start_date_controller.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 2 module Publish
  2. 2 module Courses
  3. 2 class StartDateController < PublishController
  4. 2 include CourseBasicDetailConcern
  5. 2 private
  6. 2 def current_step
  7. 8 :start_date
  8. end
  9. 2 def error_keys
  10. 3 [:start_date]
  11. end
  12. end
  13. end
  14. end

app/controllers/publish/courses/study_mode_controller.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. 4 module Publish
  2. 4 module Courses
  3. 4 class StudyModeController < PublishController
  4. 4 include CourseBasicDetailConcern
  5. 4 def edit
  6. 2 authorize(provider)
  7. 2 @course_study_mode_form = CourseStudyModeForm.new(@course)
  8. end
  9. 4 def update
  10. 2 authorize(provider)
  11. 2 @course_study_mode_form = CourseStudyModeForm.new(@course, params: study_mode_params)
  12. 2 if @course_study_mode_form.save!
  13. 1 course_description_success_message("full or part time")
  14. 1 redirect_to details_publish_provider_recruitment_cycle_course_path(
  15. provider.provider_code,
  16. recruitment_cycle.year,
  17. course.course_code,
  18. )
  19. else
  20. 1 render :edit
  21. end
  22. end
  23. 4 private
  24. 4 def study_mode_params
  25. 2 return { study_mode: nil } if params[:publish_course_study_mode_form].blank?
  26. 1 params.require(:publish_course_study_mode_form).permit(*CourseStudyModeForm::FIELDS)
  27. end
  28. 4 def current_step
  29. 19 :full_or_part_time
  30. end
  31. 4 def errors
  32. 5 params.dig(:course, :study_mode) ? {} : { study_mode: ["Pick full time, part time or full time and part time"] }
  33. end
  34. end
  35. end
  36. end

app/controllers/publish/courses/subjects_controller.rb

91.84% lines covered

49 relevant lines. 45 lines covered and 4 lines missed.
    
  1. 3 module Publish
  2. 3 module Courses
  3. 3 class SubjectsController < PublishController
  4. 3 decorates_assigned :course
  5. 3 before_action :build_course, only: %i[edit update]
  6. 3 before_action :build_course_params, only: [:continue]
  7. 3 include CourseBasicDetailConcern
  8. 3 def edit
  9. 3 authorize(provider)
  10. end
  11. 3 def continue
  12. 5 super
  13. end
  14. 3 def update
  15. 3 authorize(provider)
  16. 3 if selected_subject_ids.include?(modern_languages_subject_id.to_s)
  17. 1 redirect_to(
  18. modern_languages_publish_provider_recruitment_cycle_course_path(
  19. @course.provider_code,
  20. @course.recruitment_cycle_year,
  21. @course.course_code,
  22. course: { subjects_ids: selected_subject_ids },
  23. ),
  24. )
  25. 2 elsif course_subjects_form.save!
  26. 2 value = @course.is_primary? ? "primary subject" : "secondary subject"
  27. 2 course_details_success_message(value)
  28. 2 redirect_to(
  29. details_publish_provider_recruitment_cycle_course_path(
  30. @course.provider_code,
  31. @course.recruitment_cycle_year,
  32. @course.course_code,
  33. ),
  34. )
  35. else
  36. @errors = @course.errors.messages
  37. render :edit
  38. end
  39. end
  40. 3 private
  41. 3 def course_subjects_form
  42. 2 @course_subjects_form ||= CourseSubjectsForm.new(@course, params: selected_subject_ids)
  43. end
  44. 3 def modern_languages_subject_id
  45. 8 @modern_languages_subject_id ||= @course.edit_course_options[:modern_languages_subject].id
  46. end
  47. 3 def selected_subject_ids
  48. 21 @selected_subject_ids ||= [selected_master, selected_subordinate].compact
  49. end
  50. 3 def current_step
  51. 17 :subjects
  52. end
  53. 3 def error_keys
  54. 42 [:subjects]
  55. end
  56. 3 def selected_master
  57. 8 @selected_master ||= params[:course][:master_subject_id] if params[:course][:master_subject_id].present?
  58. end
  59. 3 def selected_subordinate
  60. 8 @selected_subordinate ||= params[:course][:subordinate_subject_id] if params[:course][:subordinate_subject_id].present?
  61. end
  62. 3 def build_course_params
  63. 5 previous_subject_selections = params[:course][:subjects_ids]
  64. 5 params[:course][:subjects_ids] = selected_subject_ids
  65. 5 params[:course].delete(:master_subject_id)
  66. 5 params[:course].delete(:subordinate_subject_id)
  67. 5 build_new_course # to get languages edit_options
  68. 5 previous_language_selections = selected_subject_ids.include?(modern_languages_subject_id.to_s) ? strip_non_language_subject_ids(previous_subject_selections) : []
  69. 5 params[:course][:subjects_ids] = selected_subject_ids.concat(previous_language_selections)
  70. end
  71. 3 def strip_non_language_subject_ids(subject_ids)
  72. 1 return [] unless subject_ids
  73. subject_ids.filter { |id| available_languages_ids.include?(id) }
  74. end
  75. 3 def available_languages_ids
  76. @course.edit_course_options[:modern_languages].map(&:id).map(&:to_s)
  77. end
  78. end
  79. end
  80. end

app/controllers/publish/courses/vacancies_controller.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 module Courses
  3. 1 class VacanciesController < PublishController
  4. 1 def edit
  5. 15 authorize(provider)
  6. 15 @course_vacancies_form = CourseVacanciesForm.new(course)
  7. 15 @site_statuses = @course_vacancies_form.running_site_statuses
  8. end
  9. 1 def update
  10. 11 authorize(provider)
  11. 11 @course_vacancies_form = CourseVacanciesForm.new(course, params: vacancy_params)
  12. 11 if @course_vacancies_form.save!
  13. 9 flash[:success] = I18n.t("success.published")
  14. 9 redirect_to publish_provider_recruitment_cycle_courses_path(
  15. provider.provider_code,
  16. recruitment_cycle.year,
  17. )
  18. else
  19. 2 @site_statuses = @course_vacancies_form.running_site_statuses
  20. 2 render :edit
  21. end
  22. end
  23. 1 private
  24. 1 def course
  25. 26 @course ||= provider.courses.find_by!(course_code: params[:code])
  26. end
  27. 1 def vacancy_params
  28. 11 return { change_vacancies_confirmation: nil } if params[:publish_course_vacancies_form].blank?
  29. 9 params
  30. .require(:publish_course_vacancies_form)
  31. .permit(
  32. CourseVacanciesForm::FIELDS,
  33. site_statuses_attributes: %i[id vac_status full_time part_time],
  34. )
  35. end
  36. end
  37. end
  38. end

app/controllers/publish/courses/withdrawals_controller.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 module Courses
  3. 1 class WithdrawalsController < PublishController
  4. 1 before_action :redirect_to_courses, if: :course_withdrawn?
  5. 7 before_action :redirect_to_courses, unless: -> { course.is_published? }
  6. 1 def edit
  7. 2 authorize(provider)
  8. 2 @course_withdrawal_form = CourseWithdrawalForm.new(course)
  9. end
  10. 1 def update
  11. 2 authorize(provider)
  12. 2 @course_withdrawal_form = CourseWithdrawalForm.new(course, params: withdrawal_params)
  13. 2 if @course_withdrawal_form.save!
  14. 1 flash[:success] = "#{course.name} (#{course.course_code}) has been withdrawn"
  15. 1 redirect_to publish_provider_recruitment_cycle_courses_path(
  16. provider.provider_code,
  17. recruitment_cycle.year,
  18. )
  19. else
  20. 1 render :edit
  21. end
  22. end
  23. 1 private
  24. 1 def redirect_to_courses
  25. 2 message = if course_withdrawn?
  26. 1 "#{course.name} (#{course.course_code}) has already been withdrawn"
  27. else
  28. 1 "Courses that have not been published should be deleted not withdrawn"
  29. end
  30. 2 flash[:error] = { id: "withdraw-error", message: message }
  31. 2 redirect_to publish_provider_recruitment_cycle_courses_path(
  32. provider.provider_code,
  33. course.recruitment_cycle_year,
  34. )
  35. end
  36. 1 def course
  37. 18 @course ||= CourseDecorator.new(provider.courses.find_by!(course_code: params[:code]))
  38. end
  39. 1 def course_withdrawn?
  40. 2 course.content_status == :withdrawn
  41. end
  42. 1 def withdrawal_params
  43. 2 return { course_code: nil } if params[:publish_course_withdrawal_form].blank?
  44. 2 params
  45. .require(:publish_course_withdrawal_form)
  46. .permit(CourseWithdrawalForm::FIELDS)
  47. end
  48. end
  49. end
  50. end

app/controllers/publish/courses_controller.rb

91.18% lines covered

68 relevant lines. 62 lines covered and 6 lines missed.
    
  1. 4 module Publish
  2. 4 class CoursesController < PublishController
  3. 4 decorates_assigned :course
  4. 4 def index
  5. 27 authorize :provider, :index?
  6. 27 courses_by_accrediting_provider
  7. 27 self_accredited_courses
  8. end
  9. 4 def show
  10. 23 fetch_course
  11. 23 authorize @course
  12. 23 @errors = flash[:error_summary]
  13. 23 flash.delete(:error_summary)
  14. end
  15. 4 def details
  16. 11 fetch_course
  17. 11 authorize @course
  18. end
  19. 4 def new
  20. 2 authorize(provider, :can_create_course?)
  21. 2 return render_locations_messages unless provider.sites&.any?
  22. 2 redirect_to new_publish_provider_recruitment_cycle_courses_level_path(params[:provider_code], @recruitment_cycle.year)
  23. end
  24. 4 def create
  25. 2 authorize(provider, :can_create_course?)
  26. 2 @course = ::Courses::CreationService.call(course_params: course_params, provider: provider, next_available_course_code: true)
  27. 2 if @course.save
  28. 2 flash[:success_with_body] = { title: "Your course has been created", body: "Add the rest of your details and publish the course, so that candidates can find and apply to it." }
  29. 2 redirect_to(
  30. publish_provider_recruitment_cycle_courses_path(
  31. @course.provider_code,
  32. @course.recruitment_cycle.year,
  33. ),
  34. )
  35. else
  36. @errors = @course.errors.messages
  37. @course_creation_params = course_params
  38. render :confirmation
  39. end
  40. end
  41. 4 def confirmation
  42. 4 authorize(provider, :can_create_course?)
  43. 4 @course_creation_params = course_params
  44. 4 @course = ::Courses::CreationService.call(course_params: course_params, provider: provider)
  45. end
  46. 4 def preview
  47. 2 fetch_course
  48. 2 authorize @course
  49. end
  50. 4 def publish
  51. 4 fetch_course
  52. 4 authorize @course
  53. 4 if @course.publishable?
  54. 3 publish_course
  55. 3 flash[:success] = "Your course has been published."
  56. 3 redirect_to publish_provider_recruitment_cycle_course_path(
  57. @provider.provider_code,
  58. @course.recruitment_cycle_year,
  59. @course.course_code,
  60. )
  61. else
  62. 1 @errors = format_publish_error_messages
  63. 1 fetch_course
  64. 1 render :show
  65. end
  66. end
  67. 4 private
  68. 4 def course_params
  69. 10 if params.key? :course
  70. 10 params.require(:course)
  71. .permit(
  72. policy(Course.new).permitted_new_course_attributes,
  73. sites_ids: [],
  74. subjects_ids: [],
  75. )
  76. else
  77. ActionController::Parameters.new({}).permit(:course)
  78. end
  79. end
  80. 4 def render_locations_messages
  81. flash[:error] = { id: "locations-error", message: "You need to create at least one location before creating a course" }
  82. redirect_to new_publish_provider_recruitment_cycle_location_path(provider.provider_code, provider.recruitment_cycle_year)
  83. end
  84. 4 def fetch_course
  85. 41 @course = provider.courses.find_by!(course_code: params[:code])
  86. end
  87. 4 def provider
  88. 111 @provider ||= recruitment_cycle.providers
  89. .includes(courses: %i[sites site_statuses enrichments provider])
  90. .find_by!(provider_code: params[:provider_code])
  91. end
  92. 4 def courses_by_accrediting_provider
  93. 54 @courses_by_accrediting_provider ||= ::Courses::Fetch.by_accrediting_provider(provider)
  94. end
  95. 4 def self_accredited_courses
  96. 27 @self_accredited_courses ||= courses_by_accrediting_provider.delete(provider.provider_name)
  97. end
  98. 4 def publish_course
  99. 3 @course.publish_sites
  100. 3 @course.publish_enrichment(@current_user)
  101. 3 NotificationService::CoursePublished.call(course: @course)
  102. end
  103. 4 def format_publish_error_messages
  104. 1 @course.errors.messages.transform_values do |error_messages|
  105. 2 error_messages.map { |message| message.gsub(/^\^/, "") }
  106. end
  107. end
  108. end
  109. end

app/controllers/publish/notifications_controller.rb

96.0% lines covered

25 relevant lines. 24 lines covered and 1 lines missed.
    
  1. 1 module Publish
  2. 1 class NotificationsController < PublishController
  3. 1 skip_before_action :check_interrupt_redirects
  4. 1 def index
  5. 2 authorize(current_user, :index?)
  6. 2 @notifications_view = NotificationsView.new(request: request, current_user: current_user)
  7. 2 @notification_form = NotificationForm.new(current_user)
  8. end
  9. 1 def update
  10. 2 authorize(current_user, :update?)
  11. 2 @notification_form = NotificationForm.new(current_user, params: notification_params)
  12. 2 if @notification_form.save!
  13. 1 flash[:success] = "Email notification preferences for #{current_user.email} have been saved."
  14. 1 redirect_to redirect_to_path
  15. else
  16. 1 @notifications_view = NotificationsView.new(request: request, current_user: current_user)
  17. 1 render(:index)
  18. end
  19. end
  20. 1 private
  21. 1 def redirect_to_path
  22. 1 if permitted_params[:provider_code].present?
  23. 1 publish_provider_path(permitted_params[:provider_code])
  24. else
  25. root_path
  26. end
  27. end
  28. 1 def permitted_params
  29. 4 params.require(:publish_notification_form).permit(*NotificationForm::FIELDS, :provider_code)
  30. end
  31. 1 def notification_params
  32. 2 permitted_params.except(:provider_code).transform_values do |value|
  33. 1 ActiveModel::Type::Boolean.new.cast(value)
  34. end
  35. end
  36. end
  37. end

app/controllers/publish/providers/access_requests_controller.rb

0.0% lines covered

28 relevant lines. 0 lines covered and 28 lines missed.
    
  1. module Publish
  2. module Providers
  3. class AccessRequestsController < PublishController
  4. before_action :provider
  5. def new
  6. authorize AccessRequest
  7. @access_request = AccessRequestForm.new(user: current_user)
  8. end
  9. def create
  10. authorize AccessRequest
  11. @access_request = AccessRequestForm.new(params: access_request_params, user: current_user)
  12. if @access_request.save!
  13. redirect_to users_publish_provider_path(params[:code]),
  14. flash: { success: "Your request for access has been submitted" }
  15. else
  16. @errors = @access_request.errors.messages
  17. render :new
  18. end
  19. end
  20. private
  21. def access_request_params
  22. params.require(:publish_access_request_form).permit(
  23. *AccessRequestForm::FIELDS,
  24. )
  25. end
  26. end
  27. end
  28. end

app/controllers/publish/providers/allocations_controller.rb

91.53% lines covered

59 relevant lines. 54 lines covered and 5 lines missed.
    
  1. 3 module Publish
  2. 3 module Providers
  3. 3 class AllocationsController < PublishController
  4. 3 def index
  5. 29 authorize provider, :show?
  6. 29 @allocations_view = AllocationsView.new(
  7. allocations: allocations[Settings.allocation_cycle_year.to_s] || [], training_providers: training_providers,
  8. )
  9. end
  10. 3 def new_repeat_request
  11. 2 authorize provider, :show?
  12. 2 provider
  13. 2 training_provider
  14. 2 @allocation = RepeatRequestForm.new
  15. end
  16. 3 def edit
  17. 2 @allocation = Allocation.includes(:provider, :accredited_body)
  18. .find(params[:id])
  19. 2 authorize @allocation
  20. 2 training_provider
  21. end
  22. 3 def create
  23. 3 service = ::Allocations::Create.new(allocation_params.merge(accredited_body_id: provider.id, request_type: get_request_type(allocation_params)))
  24. 3 authorize service.object
  25. 3 @allocation = Publish::RepeatRequestForm.new(request_type: params[:request_type])
  26. 3 if @allocation.valid? && service.execute
  27. 3 redirect_to publish_provider_recruitment_cycle_allocation_path(id: service.object.id)
  28. else
  29. render :new_repeat_request
  30. end
  31. end
  32. 3 def update
  33. 2 @allocation = Allocation.find(params[:id])
  34. 2 @allocation.request_type = params[:request_type]
  35. 2 authorize @allocation
  36. 2 @allocation.save if @allocation.changed?
  37. 2 redirect_to publish_provider_recruitment_cycle_allocation_path(id: @allocation.id)
  38. end
  39. 3 def show
  40. 6 @allocation = Allocation.find(params[:id])
  41. 6 provider
  42. 6 training_provider
  43. 6 authorize @allocation
  44. end
  45. 3 def initial_request
  46. 35 authorize provider, :show?
  47. 35 provider
  48. 35 flow = InitialRequestFlow.new(params: params)
  49. 35 if request.post? && flow.valid? && flow.redirect?
  50. 5 redirect_to flow.redirect_path
  51. else
  52. 30 render flow.template, locals: flow.locals
  53. end
  54. end
  55. 3 private
  56. 3 def training_provider
  57. 10 @training_provider ||= recruitment_cycle.providers.find_by(provider_code: params[:training_provider_code])
  58. end
  59. 3 def allocation_params
  60. 6 params.slice(
  61. :number_of_places,
  62. :request_type,
  63. ).merge(
  64. provider: recruitment_cycle.providers.find_by(provider_code: params[:training_provider_code]),
  65. ).to_unsafe_hash
  66. end
  67. 3 def get_request_type(permitted_params)
  68. 3 return permitted_params[:request_type] if permitted_params[:request_type].present?
  69. case permitted_params[:number_of_places]
  70. when "0"
  71. "declined"
  72. when nil
  73. "repeat"
  74. else
  75. "initial"
  76. end
  77. end
  78. 3 def previous_recruitment_cycle
  79. 58 @previous_recruitment_cycle ||= recruitment_cycle.previous
  80. end
  81. 3 def allocations
  82. 58 @allocations ||= Allocation
  83. .includes(:provider, :accredited_body, :allocation_uplift)
  84. .where(accredited_body_code: provider.provider_code, recruitment_cycle: [previous_recruitment_cycle, recruitment_cycle])
  85. .all
  86. 21 .group_by { |a| a.provider.recruitment_cycle_year }
  87. end
  88. 3 def training_providers
  89. 29 @training_providers ||= (allocations[previous_recruitment_cycle.year.to_s] || []).filter_map { |a|
  90. 7 a.provider if a.request_type != AllocationsView::RequestType::DECLINED
  91. }.sort_by(&:provider_name)
  92. end
  93. end
  94. end
  95. end

app/controllers/publish/providers/contacts_controller.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 module Providers
  3. 1 class ContactsController < PublishController
  4. 1 def edit
  5. 2 authorize(provider, :edit?)
  6. 2 @provider_contact_form = ProviderContactForm.new(provider)
  7. end
  8. 1 def update
  9. 2 authorize(provider, :update?)
  10. 2 @provider_contact_form = ProviderContactForm.new(provider, params: provider_contact_params)
  11. 2 if @provider_contact_form.save!
  12. 1 flash[:success] = I18n.t("success.published")
  13. 1 redirect_to details_publish_provider_recruitment_cycle_path(
  14. provider.provider_code,
  15. recruitment_cycle.year,
  16. )
  17. else
  18. 1 render :edit
  19. end
  20. end
  21. 1 private
  22. 1 def provider_contact_params
  23. 2 params.require(:publish_provider_contact_form).permit(*ProviderContactForm::FIELDS)
  24. end
  25. end
  26. end
  27. end

app/controllers/publish/providers/edit_initial_allocations_controller.rb

100.0% lines covered

32 relevant lines. 32 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 module Providers
  3. 1 class EditInitialAllocationsController < PublishController
  4. 1 def edit
  5. 27 authorize allocation
  6. 27 flow = EditInitialRequestFlow.new(params: params)
  7. 27 if request.post? && flow.valid?
  8. 8 redirect_to flow.redirect_path
  9. else
  10. 19 render flow.template, locals: flow.locals
  11. end
  12. end
  13. 1 def update
  14. 1 authorize allocation
  15. 1 update_allocation
  16. 1 redirect_to publish_provider_recruitment_cycle_allocation_path(
  17. provider_code: allocation.accredited_body.provider_code,
  18. recruitment_cycle_year: recruitment_cycle.year,
  19. training_provider_code: allocation.provider.provider_code,
  20. id: allocation.id,
  21. )
  22. end
  23. 1 def delete
  24. 1 authorize allocation
  25. 1 allocation.destroy
  26. 1 redirect_to publish_provider_recruitment_cycle_allocation_confirm_deletion_path(
  27. provider_code: provider.provider_code,
  28. recruitment_cycle_year: recruitment_cycle.year,
  29. training_provider_code: training_provider.provider_code,
  30. )
  31. end
  32. 1 def confirm_deletion
  33. 1 authorize provider, :show?
  34. 1 @allocation = Allocation.new(request_type: AllocationsView::RequestType::DECLINED)
  35. 1 training_provider
  36. 1 provider
  37. 1 recruitment_cycle
  38. 1 render template: "publish/providers/allocations/show"
  39. end
  40. 1 private
  41. 1 def allocation
  42. 35 @allocation ||= Allocation.includes(:provider, :accredited_body)
  43. .find(params[:id])
  44. end
  45. 1 def update_allocation
  46. 1 allocation.number_of_places = params[:number_of_places].to_i
  47. 1 allocation.save
  48. end
  49. 1 def training_provider
  50. 2 @training_provider ||= recruitment_cycle.providers.find_by(provider_code: params[:allocation_training_provider_code])
  51. end
  52. end
  53. end
  54. end

app/controllers/publish/providers/locations_controller.rb

93.55% lines covered

31 relevant lines. 29 lines covered and 2 lines missed.
    
  1. 1 module Publish
  2. 1 module Providers
  3. 1 class LocationsController < PublishController
  4. 1 def index
  5. 3 authorize provider, :can_list_sites?
  6. 3 @locations = provider.sites.sort_by(&:location_name)
  7. end
  8. 1 def new
  9. 1 authorize provider, :can_create_sites?
  10. 1 @location_form = LocationForm.new(provider.sites.new)
  11. end
  12. 1 def create
  13. 1 authorize provider, :can_create_sites?
  14. 1 @location_form = LocationForm.new(provider.sites.new, params: site_params)
  15. 1 if @location_form.save!
  16. 1 flash[:success] = "Your location has been created"
  17. 1 redirect_to publish_provider_recruitment_cycle_locations_path(
  18. @location_form.provider_code, @location_form.recruitment_cycle_year
  19. )
  20. else
  21. render :new
  22. end
  23. end
  24. 1 def edit
  25. 1 authorize site, :update?
  26. 1 @location_form = LocationForm.new(site)
  27. end
  28. 1 def update
  29. 1 authorize provider, :update?
  30. 1 @location_form = LocationForm.new(site, params: site_params)
  31. 1 if @location_form.save!
  32. 1 flash[:success] = I18n.t("success.value_published", value: "location details")
  33. 1 redirect_to publish_provider_recruitment_cycle_locations_path(
  34. @location_form.provider_code, @location_form.recruitment_cycle_year
  35. )
  36. else
  37. render :edit
  38. end
  39. end
  40. 1 private
  41. 1 def site
  42. 3 @site ||= provider.sites.find(params[:id])
  43. end
  44. 1 def site_params
  45. 2 params.require(:publish_location_form).permit(LocationForm::FIELDS)
  46. end
  47. end
  48. end
  49. end

app/controllers/publish/providers/visas_controller.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 module Providers
  3. 1 class VisasController < PublishController
  4. 1 def edit
  5. 2 authorize(provider, :edit?)
  6. 2 @provider_visa_form = ProviderVisaForm.new(provider)
  7. end
  8. 1 def update
  9. 2 authorize(provider, :update?)
  10. 2 @provider_visa_form = ProviderVisaForm.new(provider, params: provider_visa_params)
  11. 2 if @provider_visa_form.save!
  12. 1 flash[:success] = I18n.t("success.published")
  13. 1 redirect_to details_publish_provider_recruitment_cycle_path(
  14. provider.provider_code,
  15. recruitment_cycle.year,
  16. )
  17. else
  18. 1 render :edit
  19. end
  20. end
  21. 1 private
  22. 1 def provider_visa_params
  23. 2 return { can_sponsor_student_visa: nil } if params[:publish_provider_visa_form].blank?
  24. 1 params.require(:publish_provider_visa_form).permit(*ProviderVisaForm::FIELDS).transform_values do |value|
  25. 2 ActiveModel::Type::Boolean.new.cast(value)
  26. end
  27. end
  28. end
  29. end
  30. end

app/controllers/publish/providers_controller.rb

85.96% lines covered

57 relevant lines. 49 lines covered and 8 lines missed.
    
  1. 4 module Publish
  2. 4 class ProvidersController < PublishController
  3. 4 rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
  4. 4 decorates_assigned :provider
  5. 4 def index
  6. 230 authorize :provider, :index?
  7. 230 page = (params[:page] || 1).to_i
  8. 230 per_page = 10
  9. 230 @pagy, @providers = pagy(providers.order(:provider_name), page: page, items: per_page)
  10. 230 render "publish/providers/no_providers", status: :forbidden if @providers.blank?
  11. 230 redirect_to publish_provider_path(@providers.first.provider_code) if @providers.count == 1
  12. end
  13. 4 def suggest
  14. authorize :provider, :show?
  15. @provider_list = providers
  16. .provider_search(params[:query])
  17. .limit(10)
  18. .map { |provider| { code: provider.provider_code, name: provider.provider_name } }
  19. render json: @provider_list
  20. end
  21. 4 def show
  22. 161 authorize provider
  23. 161 if FeatureService.enabled?(:new_publish_navigation)
  24. 7 redirect_to publish_provider_recruitment_cycle_courses_path(provider.provider_code, provider.recruitment_cycle_year)
  25. else
  26. 154 :show?
  27. end
  28. end
  29. 4 def details
  30. 7 authorize provider, :show?
  31. 6 redirect_to_contact_page_with_ukprn_error if provider.ukprn.blank?
  32. 6 @errors = flash[:error_summary]
  33. 6 flash.delete(:error_summary)
  34. end
  35. 4 def about
  36. 3 authorize provider, :show?
  37. 3 @about_form = AboutYourOrganisationForm.new(provider)
  38. end
  39. 4 def update
  40. 4 authorize provider, :update?
  41. 4 @about_form = AboutYourOrganisationForm.new(provider, params: provider_params)
  42. 4 if @about_form.save!
  43. 3 flash[:success] = I18n.t("success.published")
  44. 3 redirect_to(
  45. details_publish_provider_recruitment_cycle_path(
  46. provider.provider_code,
  47. provider.recruitment_cycle_year,
  48. ),
  49. )
  50. else
  51. 1 @errors = @about_form.errors.messages
  52. 1 render :about
  53. end
  54. end
  55. 4 def search
  56. 2 skip_authorization
  57. 2 provider_query = params[:query]
  58. 2 if provider_query.blank?
  59. flash[:error] = { id: "provider-error", message: "Name or provider code" }
  60. return redirect_to publish_root_path
  61. end
  62. 2 provider_code = provider_query
  63. .split
  64. .last
  65. .gsub(/[()]/, "")
  66. 2 redirect_to publish_provider_path(provider_code)
  67. end
  68. 4 private
  69. 4 def provider
  70. 208 @provider ||= recruitment_cycle.providers.find_by!(provider_code: params[:provider_code] || params[:code])
  71. end
  72. 4 def providers
  73. 230 @providers ||= if current_user.admin?
  74. 48 RecruitmentCycle.current.providers
  75. else
  76. 182 RecruitmentCycle.current.providers.where(id: current_user.providers)
  77. end
  78. end
  79. 4 def redirect_to_contact_page_with_ukprn_error
  80. flash[:error] = { id: "publish-provider-contact-form-ukprn-field", message: "Please enter a UKPRN before continuing" }
  81. redirect_to contact_publish_provider_recruitment_cycle_path(provider.provider_code, provider.recruitment_cycle_year)
  82. end
  83. 4 def provider_params
  84. 4 params
  85. .fetch(:publish_about_your_organisation_form, {})
  86. .permit(
  87. *AboutYourOrganisationForm::FIELDS,
  88. accredited_bodies: %i[provider_name provider_code description],
  89. )
  90. end
  91. end
  92. end

app/controllers/publish/publish_controller.rb

100.0% lines covered

28 relevant lines. 28 lines covered and 0 lines missed.
    
  1. 4 module Publish
  2. 4 class PublishController < ApplicationController
  3. 4 include SuccessMessage
  4. 4 layout "publish"
  5. 4 before_action :check_interrupt_redirects
  6. 4 after_action :verify_authorized
  7. 4 private
  8. 4 def provider
  9. 590 @provider ||= recruitment_cycle.providers.find_by(provider_code: params[:provider_code] || params[:code])
  10. end
  11. 4 def recruitment_cycle
  12. 698 cycle_year = params[:recruitment_cycle_year] || params[:year] || Settings.current_recruitment_cycle_year
  13. 698 @recruitment_cycle ||= RecruitmentCycle.find_by!(year: cycle_year)
  14. end
  15. 4 def show_errors_on_publish?
  16. 27 params[:display_errors].present?
  17. end
  18. 4 def check_interrupt_redirects(use_redirect_back_to: true)
  19. 886 if !current_user.accepted_terms?
  20. 8 redirect_to publish_accept_terms_path
  21. 878 elsif show_rollover_page?
  22. 2 redirect_to publish_rollover_path
  23. 876 elsif show_rollover_recruitment_page?
  24. 2 redirect_to publish_rollover_recruitment_path
  25. 874 elsif use_redirect_back_to
  26. 874 redirect_to session[:redirect_back_to] if session[:redirect_back_to].present?
  27. 874 session.delete(:redirect_back_to)
  28. end
  29. end
  30. 4 def show_rollover_page?
  31. 878 FeatureService.enabled?("rollover.can_edit_current_and_next_cycles") && current_user.current_rollover_acceptance.blank?
  32. end
  33. 4 def show_rollover_recruitment_page?
  34. 876 FeatureService.enabled?("rollover.show_next_cycle_allocation_recruitment_page") &&
  35. current_user.current_rollover_recruitment_acceptance.blank?
  36. end
  37. end
  38. end

app/controllers/publish/recruitment_cycles_controller.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. module Publish
  2. class RecruitmentCyclesController < PublishController
  3. def show
  4. authorize provider, :show?
  5. @recruitment_cycle = RecruitmentCycle.find_by(year: params[:year])
  6. @provider ||= recruitment_cycle.providers.find_by!(provider_code: params[:provider_code] || params[:code])
  7. unless @provider.rolled_over?
  8. redirect_to publish_provider_path(@provider.provider_code)
  9. end
  10. end
  11. end
  12. end

app/controllers/publish/rollover_controller.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class RolloverController < ApplicationController
  3. 1 def new; end
  4. 1 def create
  5. 1 InterruptPageAcknowledgement.find_or_create_by!(
  6. user: current_user,
  7. recruitment_cycle: RecruitmentCycle.current,
  8. page: "rollover",
  9. )
  10. 1 redirect_to publish_root_path
  11. end
  12. end
  13. end

app/controllers/publish/rollover_recruitment_controller.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class RolloverRecruitmentController < ApplicationController
  3. 1 def new; end
  4. 1 def create
  5. 1 InterruptPageAcknowledgement.find_or_create_by!(
  6. user: current_user,
  7. recruitment_cycle: RecruitmentCycle.current,
  8. page: "rollover_recruitment",
  9. )
  10. 1 redirect_to publish_root_path
  11. end
  12. end
  13. end

app/controllers/publish/terms_controller.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 2 module Publish
  2. 2 class TermsController < ApplicationController
  3. 2 skip_before_action :authenticate
  4. 2 def edit
  5. 8 @accept_terms_form = Interruption::AcceptTermsForm.new(current_user)
  6. end
  7. 2 def update
  8. 4 @accept_terms_form = Interruption::AcceptTermsForm.new(current_user, params: accept_term_params)
  9. 4 if @accept_terms_form.save!
  10. 2 redirect_to publish_root_path
  11. else
  12. 2 render :edit
  13. end
  14. end
  15. 2 private
  16. 2 def accept_term_params
  17. 4 params.require(:publish_interruption_accept_terms_form).permit(*Interruption::AcceptTermsForm::FIELDS)
  18. end
  19. end
  20. end

app/controllers/publish/training_providers/course_exports_controller.rb

0.0% lines covered

21 relevant lines. 0 lines covered and 21 lines missed.
    
  1. module Publish
  2. module TrainingProviders
  3. class CourseExportsController < PublishController
  4. def index
  5. authorize(provider, :can_list_training_providers?)
  6. respond_to do |format|
  7. format.csv do
  8. send_data(data_export.data, filename: data_export.filename, disposition: :attachment)
  9. end
  10. end
  11. end
  12. private
  13. def courses
  14. @courses ||= provider.current_accredited_courses.includes(:enrichments, :sites, :site_statuses)
  15. end
  16. def data_export
  17. @data_export ||= Exports::AccreditedCourseList.new(courses)
  18. end
  19. end
  20. end
  21. end

app/controllers/publish/training_providers/courses_controller.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 module TrainingProviders
  3. 1 class CoursesController < PublishController
  4. 1 def index
  5. 1 authorize(provider, :index?)
  6. 1 @courses = fetch_courses
  7. end
  8. 1 private
  9. 1 def training_provider
  10. 1 @training_provider ||= provider.training_providers.find_by(provider_code: params[:training_provider_code])
  11. end
  12. 1 def fetch_courses
  13. 1 training_provider
  14. .courses
  15. .includes(:enrichments, :site_statuses, provider: [:recruitment_cycle])
  16. .where(accredited_body_code: provider.provider_code)
  17. .order(:name)
  18. .map(&:decorate)
  19. end
  20. end
  21. end
  22. end

app/controllers/publish/training_providers_controller.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class TrainingProvidersController < PublishController
  3. 1 def index
  4. 2 authorize(provider, :can_list_training_providers?)
  5. 2 @training_providers = provider.training_providers.include_accredited_courses_counts(provider.provider_code).order(:provider_name)
  6. 4 @course_counts = @training_providers.to_h { |p| [p.provider_code, p.accredited_courses_count] }
  7. end
  8. end
  9. end

app/controllers/publish/users_controller.rb

0.0% lines covered

8 relevant lines. 0 lines covered and 8 lines missed.
    
  1. module Publish
  2. class UsersController < PublishController
  3. def index
  4. authorize(provider)
  5. @users = provider.users
  6. end
  7. end
  8. end

app/controllers/reporting_controller.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 class ReportingController < ActionController::API
  2. 1 before_action :build_recruitment_cycle
  3. 1 def reporting
  4. 1 render json: StatisticService.reporting(recruitment_cycle: @recruitment_cycle)
  5. end
  6. 1 private
  7. 1 def build_recruitment_cycle
  8. 1 @recruitment_cycle = RecruitmentCycle.find_by(
  9. year: params[:recruitment_cycle_year],
  10. ) || RecruitmentCycle.current_recruitment_cycle
  11. end
  12. end

app/controllers/sessions_controller.rb

95.45% lines covered

22 relevant lines. 21 lines covered and 1 lines missed.
    
  1. 4 class SessionsController < ApplicationController
  2. 4 skip_before_action :authenticate
  3. 4 def sign_out
  4. 1 if AuthenticationService.persona?
  5. redirect_to "/auth/developer/signout"
  6. else
  7. 1 redirect_to "/auth/dfe/signout"
  8. end
  9. end
  10. 4 def callback
  11. 225 UserSession.begin_session!(session, request.env["omniauth.auth"])
  12. 225 if current_user
  13. 223 UserSessions::Update.call(user: current_user, user_session: user_session)
  14. 223 redirect_to after_sign_in_path
  15. else
  16. 2 UserSession.end_session!(session)
  17. 2 redirect_to user_not_found_path
  18. end
  19. end
  20. 4 def destroy
  21. 2 if current_user.present?
  22. 1 UserSession.end_session!(session)
  23. 1 redirect_to user_session.logout_url
  24. else
  25. 1 redirect_to support_providers_path
  26. end
  27. end
  28. 4 private
  29. 4 def after_sign_in_path
  30. 223 saved_path = session.delete("post_dfe_sign_in_path")
  31. 223 saved_path || publish_root_path
  32. end
  33. end

app/controllers/sign_in_controller.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 4 class SignInController < ApplicationController
  2. 4 skip_before_action :authenticate
  3. 4 def index
  4. 228 if AuthenticationService.magic_link?
  5. 4 redirect_to magic_links_path
  6. else
  7. 224 render "index"
  8. end
  9. end
  10. end

app/controllers/support/allocation_uplifts_controller.rb

92.0% lines covered

25 relevant lines. 23 lines covered and 2 lines missed.
    
  1. 2 module Support
  2. 2 class AllocationUpliftsController < SupportController
  3. 2 def edit
  4. 1 allocation_uplift
  5. end
  6. 2 def new
  7. 1 @allocation_uplift = AllocationUplift.new(allocation: allocation)
  8. end
  9. 2 def create
  10. 1 @allocation_uplift = AllocationUplift.new(create_allocation_uplift_params)
  11. 1 @allocation_uplift.allocation = allocation
  12. 1 if @allocation_uplift.save
  13. 1 redirect_to support_allocation_path(@allocation_uplift.allocation), flash: { success: "Allocation Uplift was successfully created" }
  14. else
  15. render :new
  16. end
  17. end
  18. 2 def update
  19. 1 if allocation_uplift.update(update_allocation_uplift_params)
  20. 1 redirect_to support_allocation_path(allocation_uplift.allocation), flash: { success: "Allocation Uplift was successfully updated" }
  21. else
  22. render :edit
  23. end
  24. end
  25. 2 private
  26. 2 def allocation_uplift
  27. 3 @allocation_uplift ||= allocation.allocation_uplift
  28. end
  29. 2 def allocation
  30. 4 @allocation ||= Allocation.find(params[:allocation_id])
  31. end
  32. 2 def create_allocation_uplift_params
  33. 1 params.require(:allocation_uplift).permit(:allocation_id, :uplifts)
  34. end
  35. 2 def update_allocation_uplift_params
  36. 1 params.require(:allocation_uplift).permit(:uplifts)
  37. end
  38. end
  39. end

app/controllers/support/allocations_controller.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 3 module Support
  2. 3 class AllocationsController < SupportController
  3. 3 def index
  4. 6 @allocations = filtered_allocations
  5. end
  6. 3 def show
  7. 3 allocation
  8. end
  9. 3 private
  10. 3 def allocation
  11. 3 @allocation ||= Allocation.find(params[:id])
  12. end
  13. 3 def filtered_allocations
  14. 6 @filtered_allocations ||= Support::Filter.call(model_data_scope: Allocation.current_allocations, filter_params: filter_params)
  15. end
  16. 3 def filter_params
  17. 6 @filter_params ||= params.except(:commit).permit(:text_search, :page)
  18. end
  19. end
  20. end

app/controllers/support/courses_controller.rb

100.0% lines covered

32 relevant lines. 32 lines covered and 0 lines missed.
    
  1. 3 module Support
  2. 3 class CoursesController < SupportController
  3. 3 def index
  4. 7 @courses = provider.courses.order(:name).page(params[:page] || 1)
  5. 6 render layout: "provider_record"
  6. rescue ActiveRecord::RecordNotFound
  7. 1 flash[:warning] = "Provider not found"
  8. 1 redirect_to support_providers_path
  9. end
  10. 3 def edit
  11. 9 @edit_course_form = Support::EditCourseForm.new(course)
  12. end
  13. 3 def update
  14. 4 @edit_course_form = Support::EditCourseForm.new(course)
  15. 4 @edit_course_form.assign_attributes(update_course_params)
  16. 4 if @edit_course_form.save
  17. 1 redirect_to support_provider_courses_path(provider), flash: { success: t("support.flash.updated", resource: "Course") }
  18. else
  19. 3 render :edit
  20. end
  21. end
  22. 3 private
  23. 3 def provider
  24. 21 @provider ||= RecruitmentCycle.current.providers.find(params[:provider_id])
  25. end
  26. 3 def course
  27. 13 @course ||= provider.courses.find(params[:id])
  28. end
  29. 3 def update_course_params
  30. 4 params.require(:support_edit_course_form).permit(
  31. *EditCourseForm::FIELDS,
  32. :"start_date(3i)", :"start_date(2i)", :"start_date(1i)",
  33. :"applications_open_from(3i)", :"applications_open_from(2i)", :"applications_open_from(1i)",
  34. :is_send
  35. 36 ).transform_keys { |key| date_field_to_attribute(key) }
  36. end
  37. 3 def date_field_to_attribute(key)
  38. 36 case key
  39. 4 when "start_date(3i)" then "start_date_day"
  40. 4 when "start_date(2i)" then "start_date_month"
  41. 4 when "start_date(1i)" then "start_date_year"
  42. 4 when "applications_open_from(3i)" then "applications_open_from_day"
  43. 4 when "applications_open_from(2i)" then "applications_open_from_month"
  44. 4 when "applications_open_from(1i)" then "applications_open_from_year"
  45. 12 else key
  46. end
  47. end
  48. end
  49. end

app/controllers/support/data_exports_controller.rb

0.0% lines covered

14 relevant lines. 0 lines covered and 14 lines missed.
    
  1. module Support
  2. class DataExportsController < SupportController
  3. def index
  4. @data_exports = DataExports::DataExport.all
  5. end
  6. def download
  7. unless (@data_export = DataExports::DataExport.find(params[:id]))
  8. redirect_to action: :index, error: "Unable to find data export"
  9. return
  10. end
  11. send_data @data_export.to_csv, filename: @data_export.filename, disposition: :attachment
  12. end
  13. end
  14. end

app/controllers/support/locations_controller.rb

90.91% lines covered

33 relevant lines. 30 lines covered and 3 lines missed.
    
  1. 1 module Support
  2. 1 class LocationsController < SupportController
  3. 1 def index
  4. 8 @sites = provider.sites.order(:location_name).page(params[:page] || 1)
  5. 8 render layout: "provider_record"
  6. rescue ActiveRecord::RecordNotFound
  7. flash[:warning] = "Provider not found"
  8. redirect_to support_providers_path
  9. end
  10. 1 def new
  11. 1 @site = provider.sites.build
  12. end
  13. 1 def create
  14. 1 @site = provider.sites.build(site_params)
  15. 1 if @site.save
  16. 1 redirect_to support_provider_locations_path(provider), flash: { success: t("support.flash.created", resource: flash_resource) }
  17. else
  18. render :new
  19. end
  20. end
  21. 1 def edit
  22. 3 provider
  23. 3 site
  24. end
  25. 1 def update
  26. 2 if site.update(site_params)
  27. 1 redirect_to support_provider_locations_path(provider), flash: { success: t("support.flash.updated", resource: flash_resource) }
  28. else
  29. 1 render :edit
  30. end
  31. end
  32. 1 def destroy
  33. 1 site.destroy!
  34. 1 redirect_to support_provider_locations_path(provider), flash: { success: t("support.flash.deleted", resource: flash_resource) }
  35. end
  36. 1 private
  37. 1 def provider
  38. 22 @provider ||= RecruitmentCycle.current.providers.find(params[:provider_id])
  39. end
  40. 1 def flash_resource
  41. 3 @flash_resource ||= "Location"
  42. end
  43. 1 def site_params
  44. 3 params.require(:site).permit(
  45. :location_name,
  46. :urn,
  47. :code,
  48. :address1,
  49. :address2,
  50. :address3,
  51. :address4,
  52. :postcode,
  53. )
  54. end
  55. 1 def site
  56. 6 @site ||= provider.sites.find(params[:id])
  57. end
  58. end
  59. end

app/controllers/support/providers_controller.rb

100.0% lines covered

42 relevant lines. 42 lines covered and 0 lines missed.
    
  1. 4 module Support
  2. 4 class ProvidersController < SupportController
  3. 4 def index
  4. 21 @providers = filtered_providers
  5. end
  6. 4 def new
  7. 2 @provider = Provider.new
  8. 2 @provider.sites.build
  9. 2 @provider.organisations.build
  10. end
  11. 4 def create
  12. 2 @provider = Provider.new(create_provider_params)
  13. 2 if @provider.save
  14. 1 redirect_to support_provider_path(provider), flash: { success: "Provider was successfully created" }
  15. else
  16. # The below code is to fix a mismatch of error messages
  17. # for invalid forms in the support console.
  18. 1 @provider.errors.messages.each { |k, v|
  19. 8 case k
  20. when :"sites.urn", :email, :telephone
  21. 2 @provider.errors.messages[k] = v.first.gsub("^", "")
  22. else
  23. 6 @provider.errors.messages[k] = "#{k.to_s.gsub(/.\.^?/, ' ').humanize} #{v.first}"
  24. end
  25. }
  26. 1 render :new
  27. end
  28. end
  29. 4 def show
  30. 8 provider
  31. 8 render layout: "provider_record"
  32. end
  33. 4 def edit
  34. 4 provider
  35. end
  36. 4 def update
  37. 2 if provider.update(update_provider_params)
  38. 1 redirect_to support_provider_path(provider), flash: { success: t("support.flash.updated", resource: "Provider") }
  39. else
  40. 1 render :edit
  41. end
  42. end
  43. 4 def users
  44. 4 @users = provider.users.order(:last_name).page(params[:page] || 1)
  45. 4 render layout: "provider_record"
  46. end
  47. 4 private
  48. 4 def filtered_providers
  49. 21 @filtered_providers ||= Support::Filter.call(model_data_scope: find_providers, filter_params: filter_params)
  50. end
  51. 4 def find_providers
  52. 21 RecruitmentCycle.current.providers.order(:provider_name).includes(:courses, :users)
  53. end
  54. 4 def filter_params
  55. 21 @filter_params ||= params.except(:commit).permit(:provider_search, :course_search, :page)
  56. end
  57. 4 def provider
  58. 20 @provider ||= Provider.find(params[:id])
  59. end
  60. 4 def update_provider_params
  61. 2 params.require(:provider).permit(:provider_name, :provider_type)
  62. end
  63. 4 def create_provider_params
  64. 2 params.require(:provider).permit(:provider_name,
  65. :provider_code,
  66. :provider_type,
  67. :urn,
  68. :recruitment_cycle_id,
  69. :email,
  70. :ukprn,
  71. :telephone, sites_attributes: %i[code
  72. urn
  73. location_name
  74. address1
  75. address2
  76. address3
  77. address4
  78. postcode],
  79. organisations_attributes: %i[name]).merge(recruitment_cycle: RecruitmentCycle.current_recruitment_cycle)
  80. end
  81. end
  82. end

app/controllers/support/support_controller.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 4 module Support
  2. 4 class SupportController < ApplicationController
  3. 4 layout "support"
  4. 4 before_action :check_user_is_admin
  5. 4 private
  6. 4 def check_user_is_admin
  7. 122 if !current_user.admin?
  8. 1 flash[:warning] = "User is not an admin"
  9. 1 redirect_to sign_in_path
  10. end
  11. end
  12. end
  13. end

app/controllers/support/user_permissions_controller.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 1 module Support
  2. 1 class UserPermissionsController < SupportController
  3. 1 def destroy
  4. 2 user.remove_access_to(provider)
  5. 2 redirect_to origin_page, flash: { success: t("support.flash.deleted", resource: flash_resource) }
  6. end
  7. 1 private
  8. 1 def user_permission
  9. 4 @user_permission ||= UserPermission.find(params[:id])
  10. end
  11. 1 def user
  12. 2 @user ||= user_permission.user
  13. end
  14. 1 def provider
  15. 2 @provider ||= user_permission.provider
  16. end
  17. 1 def origin_page
  18. 2 request.referer
  19. end
  20. 1 def flash_resource
  21. 2 @flash_resource ||= "User permission"
  22. end
  23. end
  24. end

app/controllers/support/users/providers_controller.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 1 module Support
  2. 1 module Users
  3. 1 class ProvidersController < SupportController
  4. 1 def show
  5. 2 user
  6. 2 @providers = providers.order(:provider_name).page(params[:page] || 1)
  7. 2 render layout: "user_record"
  8. end
  9. 1 private
  10. 1 def user
  11. 4 @user ||= User.find(params[:user_id])
  12. end
  13. 1 def providers
  14. 2 RecruitmentCycle.current.providers.where(id: user.providers)
  15. end
  16. end
  17. end
  18. end

app/controllers/support/users_controller.rb

82.86% lines covered

35 relevant lines. 29 lines covered and 6 lines missed.
    
  1. 2 module Support
  2. 2 class UsersController < SupportController
  3. 2 def index
  4. 15 @users = filtered_users.page(params[:page] || 1)
  5. end
  6. 2 def show
  7. 3 user
  8. 3 render layout: "user_record"
  9. end
  10. 2 def new
  11. 4 @user = User.new
  12. end
  13. 2 def create
  14. 2 @user = User.new(user_params)
  15. 2 if @user.save
  16. 1 redirect_to support_users_path
  17. else
  18. 1 render :new
  19. end
  20. end
  21. 2 def edit
  22. user
  23. end
  24. 2 def update
  25. if user.update(update_user_params)
  26. redirect_to support_user_path(user), flash: { success: t("support.flash.updated", resource: "User") }
  27. else
  28. render :edit
  29. end
  30. end
  31. 2 def destroy
  32. 1 if user.discard
  33. 1 redirect_to support_users_path, flash: { success: "User successfully deleted" }
  34. else
  35. redirect_to support_users_path, flash: { success: "This user has already been deleted" }
  36. end
  37. end
  38. 2 private
  39. 2 def user_params
  40. 2 params.require(:user).permit(:first_name, :last_name, :email).merge(state: "new")
  41. end
  42. 2 def update_user_params
  43. params.require(:user).permit(:first_name, :last_name, :email, :admin)
  44. end
  45. 2 def user
  46. 4 @user ||= User.find(params[:id])
  47. end
  48. 2 def filtered_users
  49. 15 Support::Filter.call(model_data_scope: User.order(:last_name), filter_params: filter_params)
  50. end
  51. 2 def filter_params
  52. 15 @filter_params ||= params.except(:commit).permit(:text_search, :page, :commit, user_type: [])
  53. end
  54. end
  55. end

app/decorators/application_decorator.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 4 class ApplicationDecorator < Draper::Decorator
  2. # TODO: Move this to a view component
  3. 4 def status_tag
  4. 45 tag = h.govuk_tag(text: status_text.html_safe, colour: status_colour)
  5. 45 tag += unpublished_status_hint if object.has_unpublished_changes?
  6. 45 tag.html_safe
  7. end
  8. 4 private
  9. 4 def status_text
  10. 45 return status_tags[:withdrawn][:text] if object.ucas_status == "not_running"
  11. 45 status_tags[object.content_status.to_sym][:text]
  12. end
  13. 4 def status_colour
  14. 45 return status_tags[:withdrawn][:colour] if object.ucas_status == "not_running"
  15. 45 status_tags[object.content_status.to_sym][:colour]
  16. end
  17. 4 def status_tags
  18. {
  19. 90 published: { text: "Published", colour: "green" },
  20. withdrawn: { text: "Withdrawn", colour: "red" },
  21. empty: { text: "Empty", colour: "grey" },
  22. draft: { text: "Draft", colour: "yellow" },
  23. published_with_unpublished_changes: { text: "Published&nbsp;*", colour: "green" },
  24. rolled_over: { text: "Rolled over", colour: "grey" },
  25. }
  26. end
  27. 4 def unpublished_status_hint
  28. 4 h.tag.span("*&nbsp;Unpublished&nbsp;changes".html_safe, class: "govuk-body-s govuk-!-display-block govuk-!-margin-bottom-0 govuk-!-margin-top-1")
  29. end
  30. end

app/decorators/course_decorator.rb

89.94% lines covered

169 relevant lines. 152 lines covered and 17 lines missed.
    
  1. 4 class CourseDecorator < ApplicationDecorator
  2. 4 delegate_all
  3. 4 def name_and_code
  4. 260 "#{object.name} (#{object.course_code})"
  5. end
  6. 4 def vacancies
  7. 17 content = object.has_vacancies? ? "Yes" : "No"
  8. 17 content += " (#{edit_vacancy_link})" unless object.is_withdrawn?
  9. 17 content.html_safe
  10. end
  11. 4 def find_url(provider = object.provider)
  12. 4 h.search_ui_course_page_url(provider_code: provider.provider_code, course_code: object.course_code)
  13. end
  14. 4 def on_find(provider = object.provider)
  15. 45 if object.findable?
  16. 13 if current_cycle_and_open?
  17. 13 h.govuk_link_to("Yes - view online", h.search_ui_course_page_url(provider_code: provider.provider_code, course_code: object.course_code))
  18. else
  19. "Yes - from #{Date::MONTHNAMES[Settings.next_cycle_open_date.to_date.month]}"
  20. end
  21. else
  22. 32 not_on_find
  23. end
  24. end
  25. 4 def open_or_closed_for_applications
  26. 14 object.open_for_applications? ? "Open" : "Closed"
  27. end
  28. 4 def outcome
  29. 21 I18n.t("edit_options.qualifications.#{object.qualification}.label")
  30. end
  31. 4 def is_send?
  32. 16 object.is_send? ? "Yes" : "No"
  33. end
  34. 4 def funding
  35. {
  36. 14 "salary" => "Salaried",
  37. "apprenticeship" => "Teaching apprenticeship (with salary)",
  38. "fee" => "Fee paying (no salary)",
  39. }[object.funding_type]
  40. end
  41. 4 def subject_name
  42. 3 if object.subjects.size == 1
  43. 2 object.subjects.first.subject_name
  44. else
  45. 1 object.name
  46. end
  47. end
  48. 4 def has_scholarship_and_bursary?
  49. 3 object.has_bursary? && object.has_scholarship?
  50. end
  51. 4 def bursary_first_line_ending
  52. if bursary_requirements.count > 1
  53. ":"
  54. else
  55. "#{bursary_requirements.first}."
  56. end
  57. end
  58. # def bursary_requirements
  59. # requirements = ["a degree of 2:2 or above in any subject"]
  60. # if object.subjects.any? { |subject| subject.subject_name.downcase == "primary with mathematics" }
  61. # mathematics_requirement = "at least grade B in maths A-level (or an equivalent)"
  62. # requirements.push(mathematics_requirement)
  63. # end
  64. # requirements
  65. # end
  66. 4 def bursary_only?
  67. 1 object.has_bursary? && !object.has_scholarship?
  68. end
  69. 4 def excluded_from_bursary?
  70. 3 object.subjects.present? &&
  71. # incorrect bursary eligibility only shows up on courses with 2 subjects
  72. object.subjects.count == 2 &&
  73. has_excluded_course_name?
  74. end
  75. # def has_early_career_payments?
  76. # object.subjects.present? &&
  77. # object.subjects.any? { |subject| subject.attributes["early_career_payments"].present? }
  78. # end
  79. # def bursary_amount
  80. # find_max("bursary_amount")
  81. # end
  82. # def scholarship_amount
  83. # find_max("scholarship")
  84. # end
  85. 4 def salaried?
  86. 9 object.funding_type == "salary" || object.funding_type == "apprenticeship"
  87. end
  88. 4 def apprenticeship?
  89. 2 object.funding_type.to_s == "apprenticeship" ? "Yes" : "No"
  90. end
  91. 4 def sorted_subjects
  92. 16 object.subjects.map(&:subject_name).sort.join("<br>").html_safe
  93. end
  94. 4 def length
  95. 29 case course_length.to_s
  96. when "OneYear"
  97. 1 "1 year"
  98. when "TwoYears"
  99. 7 "Up to 2 years"
  100. else
  101. 21 course_length.to_s
  102. end
  103. end
  104. 4 def other_course_length?
  105. 3 %w[OneYear TwoYears].exclude?(course_length) && !course_length.nil?
  106. end
  107. 4 def other_age_range?
  108. 36 options = object.edit_course_options["age_range_in_years"]
  109. 36 options.exclude?(course.age_range_in_years)
  110. end
  111. 4 def alphabetically_sorted_sites
  112. 1 object.sites.sort_by(&:location_name)
  113. end
  114. 4 def preview_site_statuses
  115. 11 object.site_statuses.new_or_running.sort_by { |status| status.site.location_name }
  116. end
  117. 4 def has_site?(site)
  118. 11 !course.sites.nil? && object.sites.any? { |s| s.id == site.id }
  119. end
  120. # rubocop:disable Lint/DuplicateBranch: Duplicate branch body detected
  121. 4 def funding_option
  122. 3 if salaried?
  123. 1 "Salary"
  124. 2 elsif excluded_from_bursary?
  125. # Duplicate branch body detected
  126. "Student finance if you’re eligible"
  127. 2 elsif has_scholarship_and_bursary?
  128. 2 "Scholarships or bursaries, as well as student finance, are available if you’re eligible"
  129. elsif has_bursary?
  130. "Bursaries and student finance are available if you’re eligible"
  131. else
  132. # Duplicate branch body detected
  133. "Student finance if you’re eligible"
  134. end
  135. end
  136. # rubocop:enable Lint/DuplicateBranch: Duplicate branch body detected
  137. 4 def current_cycle?
  138. 21 course.recruitment_cycle.year.to_i == Settings.current_recruitment_cycle_year
  139. end
  140. 4 def current_cycle_and_open?
  141. 13 current_cycle? && FeatureService.enabled?("rollover.has_current_cycle_started?")
  142. end
  143. 4 def next_cycle?
  144. 17 course.recruitment_cycle.year.to_i == Settings.current_recruitment_cycle_year + 1
  145. end
  146. 4 def use_financial_support_placeholder?
  147. 2 course.recruitment_cycle.year.to_i == Settings.financial_support_placeholder_cycle
  148. end
  149. 4 def cycle_range
  150. 2 "#{course.recruitment_cycle.year} to #{course.recruitment_cycle.year.to_i + 1}"
  151. end
  152. 4 def age_range
  153. 15 if object.age_range_in_years.present?
  154. 15 I18n.t("edit_options.age_range_in_years.#{object.age_range_in_years}.label", default: object.age_range_in_years.humanize)
  155. else
  156. "<span class='app-!-colour-muted'>Unknown</span>".html_safe
  157. end
  158. end
  159. 4 def applications_open_from_message_for(recruitment_cycle)
  160. 6 if current_cycle?
  161. 6 "As soon as the course is on Find (recommended)"
  162. else
  163. year = recruitment_cycle.year.to_i
  164. day_month = recruitment_cycle.application_start_date.strftime("%-d %B")
  165. "On #{day_month} when applications for the #{year} to #{year + 1} cycle open"
  166. end
  167. end
  168. 4 def selectable_subjects
  169. 356 edit_course_options["subjects"].map { |subject| [subject.attributes["subject_name"], subject["id"]] }
  170. end
  171. 4 def selected_subject_ids
  172. 29 selectable_subject_ids = course.subjects.map { |subject| subject["id"] }
  173. 22 selected_subject_ids = subjects.map(&:id)
  174. 22 selectable_subject_ids & selected_subject_ids
  175. end
  176. 4 def subject_present?(subject_to_find)
  177. 74 subjects.any? do |course_subject|
  178. 75 course_subject.id == subject_to_find.id
  179. end
  180. end
  181. 4 def return_start_date
  182. 7 if FeatureService.enabled?("rollover.can_edit_current_and_next_cycles")
  183. 1 start_date.presence || "September #{Settings.current_recruitment_cycle_year + 1}"
  184. else
  185. 6 start_date.presence || "September #{Settings.current_recruitment_cycle_year}"
  186. end
  187. end
  188. 4 def placements_heading
  189. 60 if is_further_education?
  190. 1 "How teaching placements work"
  191. else
  192. 59 "How school placements work"
  193. end
  194. end
  195. 4 def listing_basic_details
  196. 13 if is_further_education?
  197. 1 ["outcome",
  198. "full time or part time",
  199. "fee or salary",
  200. "application open date",
  201. "course start date"]
  202. else
  203. 12 ["age range",
  204. "outcome",
  205. "full time or part time",
  206. "application open date",
  207. "course start date",
  208. "GCSE requirements"]
  209. end
  210. end
  211. 4 def subject_page_title
  212. 24 case level
  213. when "primary"
  214. 14 "Pick a primary subject"
  215. when "secondary"
  216. 9 "Pick a secondary subject"
  217. else
  218. 1 "Pick a subject"
  219. end
  220. end
  221. 4 def subject_input_label
  222. 15 case level
  223. when "primary"
  224. 8 "Primary subject"
  225. when "secondary"
  226. 6 "Secondary subject"
  227. else
  228. 1 "Pick a subject"
  229. end
  230. end
  231. # def accept_gcse_equivalency?
  232. # object.accept_gcse_equivalency
  233. # end
  234. 4 def has_fees?
  235. 50 object.funding_type.match?(/fee/)
  236. end
  237. 4 def is_further_education?
  238. 91 object.level.match?(/further_education/)
  239. end
  240. 4 def degree_section_complete?
  241. 61 object.degree_grade.present?
  242. end
  243. 4 def gcse_section_complete?
  244. 64 !object.accept_pending_gcse.nil? && !object.accept_gcse_equivalency.nil?
  245. end
  246. 4 def about_course
  247. 28 object.enrichment_attribute(:about_course)
  248. end
  249. 4 def interview_process
  250. 30 object.enrichment_attribute(:interview_process)
  251. end
  252. 4 def how_school_placements_work
  253. 28 object.enrichment_attribute(:how_school_placements_work)
  254. end
  255. 4 def fee_uk_eu
  256. 3 object.enrichment_attribute(:fee_uk_eu)
  257. end
  258. 4 def fee_international
  259. 3 object.enrichment_attribute(:fee_international)
  260. end
  261. 4 def fee_details
  262. 4 object.enrichment_attribute(:fee_details)
  263. end
  264. 4 def financial_support
  265. 4 object.enrichment_attribute(:financial_support)
  266. end
  267. 4 def salary_details
  268. 24 object.enrichment_attribute(:salary_details)
  269. end
  270. 4 def personal_qualities
  271. 28 object.enrichment_attribute(:personal_qualities)
  272. end
  273. 4 def other_requirements
  274. 28 object.enrichment_attribute(:other_requirements)
  275. end
  276. 4 def course_length
  277. 55 object.enrichment_attribute(:course_length)
  278. end
  279. 4 def about_accrediting_body
  280. 4 object.accrediting_provider_description
  281. end
  282. 4 def has_physical_education_subject?
  283. subjects.map(&:subject_name).include?("Physical education")
  284. end
  285. 4 private
  286. 4 def not_on_find
  287. 32 if object.new_and_not_running?
  288. 31 "No - still in draft"
  289. 1 elsif object.is_withdrawn?
  290. 1 "No - withdrawn"
  291. else
  292. "No"
  293. end
  294. end
  295. 4 def edit_vacancy_link
  296. 14 h.govuk_link_to(h.vacancies_publish_provider_recruitment_cycle_course_path(object.provider_code, object.recruitment_cycle.year, object.course_code)) do
  297. 14 h.raw("Change<span class=\"govuk-visually-hidden\"> vacancies for #{name_and_code}</span>")
  298. end
  299. end
  300. # def find_max(attribute)
  301. # subject_attributes = object.subjects.map do |s|
  302. # if s.attributes[attribute].present?
  303. # s.__send__(attribute).to_i
  304. # end
  305. # end
  306. # subject_attributes.compact.max.to_s
  307. # end
  308. 4 def has_excluded_course_name?
  309. exclusions = [
  310. /^Drama/,
  311. /^Media Studies/,
  312. /^PE/,
  313. /^Physical/,
  314. ]
  315. # We only care about course with a name matching the pattern 'Foo with bar'
  316. # We don't care about courses matching the pattern 'Foo and bar'
  317. return false unless /with/.match?(object.name)
  318. exclusions.any? { |e| e.match?(object.name) }
  319. end
  320. end

app/decorators/provider_decorator.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. class ProviderDecorator < ApplicationDecorator
  2. delegate_all
  3. def accredited_bodies
  4. object.accredited_bodies.sort_by { |provider| provider["provider_name"] }.map do |provider|
  5. Struct.new(:provider_name, :provider_code, :description, keyword_init: true).new(provider)
  6. end
  7. end
  8. def website
  9. return if object.website.blank?
  10. object.website.start_with?("http") ? object.website : "http://#{object.website}"
  11. end
  12. end

app/deserializers/api/v3/deserializable_access_request.rb

0.0% lines covered

12 relevant lines. 0 lines covered and 12 lines missed.
    
  1. module API
  2. module V3
  3. class DeserializableAccessRequest < JSONAPI::Deserializable::Resource
  4. attributes :first_name,
  5. :last_name,
  6. :email_address,
  7. :organisation,
  8. :reason,
  9. :requester_email
  10. end
  11. end
  12. end

app/deserializers/api/v3/deserializable_contact.rb

0.0% lines covered

7 relevant lines. 0 lines covered and 7 lines missed.
    
  1. module API
  2. module V3
  3. class DeserializableContact < JSONAPI::Deserializable::Resource
  4. attributes :name, :email, :telephone, :permission_given, :type
  5. end
  6. end
  7. end

app/deserializers/api/v3/deserializable_course.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 module API
  2. 1 module V3
  3. 1 class DeserializableCourse < JSONAPI::Deserializable::Resource
  4. 1 COURSE_ATTRIBUTES = %i[
  5. about_course
  6. course_length
  7. fee_details
  8. fee_international
  9. fee_uk_eu
  10. financial_support
  11. how_school_placements_work
  12. interview_process
  13. other_requirements
  14. personal_qualities
  15. salary_details
  16. required_qualifications
  17. course_code
  18. name
  19. study_mode
  20. qualifications
  21. english
  22. maths
  23. science
  24. qualification
  25. age_range_in_years
  26. start_date
  27. applications_open_from
  28. study_mode
  29. is_send
  30. accredited_body_code
  31. funding_type
  32. level
  33. degree_grade
  34. additional_degree_subject_requirements
  35. degree_subject_requirements
  36. accept_pending_gcse
  37. accept_gcse_equivalency
  38. accept_english_gcse_equivalency
  39. accept_maths_gcse_equivalency
  40. accept_science_gcse_equivalency
  41. additional_gcse_equivalencies
  42. ].freeze
  43. 1 attributes(*COURSE_ATTRIBUTES)
  44. 1 has_many :sites
  45. 1 has_many :subjects
  46. 1 def reverse_mapping
  47. 1 declared_attributes = DeserializableCourse.attr_blocks.keys
  48. 1 declared_attributes
  49. 36 .to_h { |key| [key.to_sym, "/data/attributes/#{key}"] }
  50. end
  51. end
  52. end
  53. end

app/deserializers/api/v3/deserializable_provider.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 module API
  2. 1 module V3
  3. 1 class DeserializableProvider < JSONAPI::Deserializable::Resource
  4. 1 PROVIDER_ATTRIBUTES = %i[
  5. train_with_us
  6. train_with_disability
  7. provider_name
  8. email
  9. telephone
  10. website
  11. address1
  12. address2
  13. address3
  14. address4
  15. postcode
  16. region_code
  17. accredited_bodies
  18. admin_contact
  19. utt_contact
  20. web_link_contact
  21. fraud_contact
  22. finance_contact
  23. type_of_gt12
  24. gt12_contact
  25. send_application_alerts
  26. application_alert_contact
  27. ukprn
  28. urn
  29. can_sponsor_skilled_worker_visa
  30. can_sponsor_student_visa
  31. ].freeze
  32. 1 attributes(*PROVIDER_ATTRIBUTES)
  33. 1 has_many :sites
  34. 1 has_many :courses
  35. 1 def reverse_mapping
  36. 1 declared_attributes = DeserializableProvider.attr_blocks.keys
  37. 1 declared_attributes
  38. 26 .to_h { |key| [key.to_sym, "/data/attributes/#{key}"] }
  39. end
  40. end
  41. end
  42. end

app/deserializers/api/v3/deserializable_site.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 module API
  2. 1 module V3
  3. 1 class DeserializableSite < JSONAPI::Deserializable::Resource
  4. 1 attributes :address1,
  5. :address2,
  6. :address3,
  7. :address4,
  8. :code,
  9. :location_name,
  10. :postcode,
  11. :region_code,
  12. :urn
  13. end
  14. end
  15. end

app/flows/edit_initial_request_flow.rb

96.0% lines covered

50 relevant lines. 48 lines covered and 2 lines missed.
    
  1. 1 class EditInitialRequestFlow
  2. 1 include Rails.application.routes.url_helpers
  3. 1 attr_reader :params
  4. 1 delegate :valid?, to: :form_object
  5. 1 def initialize(params:)
  6. 27 @params = params
  7. end
  8. 1 def template
  9. 19 if can_proceed_to_check_answers_page?
  10. 2 "publish/providers/allocations/edit_initial_allocations/check_answers"
  11. 17 elsif can_proceed_to_number_of_places_page?
  12. 10 "publish/providers/allocations/edit_initial_allocations/number_of_places"
  13. else
  14. 7 "publish/providers/allocations/edit_initial_allocations/do_you_want"
  15. end
  16. end
  17. 1 def locals
  18. {
  19. 19 training_provider: training_provider,
  20. provider: provider,
  21. form_object: form_object,
  22. recruitment_cycle: recruitment_cycle,
  23. allocation: allocation,
  24. }
  25. end
  26. 1 def redirect_path
  27. 8 if can_proceed_to_check_answers_page? || accepted_initial_allocation?
  28. 7 publish_provider_recruitment_cycle_allocation_get_edit_initial_request_path(
  29. provider_code: allocation.accredited_body.provider_code,
  30. recruitment_cycle_year: recruitment_cycle.year,
  31. allocation_training_provider_code: allocation.provider.provider_code,
  32. number_of_places: params[:number_of_places],
  33. next_step: params[:next_step],
  34. id: allocation.id,
  35. request_type: params[:request_type],
  36. )
  37. else
  38. 1 publish_provider_recruitment_cycle_allocation_delete_initial_request_path(
  39. provider_code: provider.provider_code,
  40. recruitment_cycle_year: recruitment_cycle.year,
  41. allocation_training_provider_code: training_provider.provider_code,
  42. id: allocation.id,
  43. )
  44. end
  45. end
  46. 1 private
  47. 1 def accepted_initial_allocation?
  48. 6 params[:next_step] == "number_of_places" && params[:request_type].present? && params[:request_type] == AllocationsView::RequestType::INITIAL
  49. end
  50. 1 def number_of_places_validation_error?
  51. 11 params[:next_step] == "check_answers" && !form_object.valid?
  52. end
  53. 1 def can_proceed_to_number_of_places_page?
  54. 17 (params[:next_step] == "number_of_places" && params[:request_type].present?) || number_of_places_validation_error?
  55. end
  56. 1 def can_proceed_to_check_answers_page?
  57. 27 params[:next_step] == "check_answers" && params[:number_of_places].present? && form_object.valid?
  58. end
  59. 1 def number_of_places_page?
  60. (params[:step].present? && params[:step] == "number_of_places") || params[:number_of_places].present?
  61. end
  62. 1 def check_answers_page?
  63. params[:step].present? && params[:step] == "check_answers"
  64. end
  65. 1 def training_provider
  66. 20 @training_provider ||= recruitment_cycle.providers.find_by(provider_code: params[:allocation_training_provider_code].upcase)
  67. end
  68. 1 def allocation
  69. 41 @allocation ||= Allocation.includes(:provider, :accredited_body)
  70. .find(params[:id])
  71. end
  72. 1 def recruitment_cycle
  73. 67 cycle_year = params[:recruitment_cycle_year] || Settings.current_recruitment_cycle_year
  74. 67 @recruitment_cycle ||= RecruitmentCycle.find_by!(year: cycle_year)
  75. end
  76. 1 def provider
  77. 20 @provider ||= recruitment_cycle.providers.find_by(provider_code: params[:provider_code])
  78. end
  79. 1 def form_object
  80. 43 if params[:number_of_places]
  81. 24 number_of_places_form_object
  82. else
  83. 19 request_form_object
  84. end
  85. end
  86. 1 def request_form_object
  87. 19 permitted_params = params
  88. .slice(:request_type)
  89. .permit(:request_type)
  90. 19 @request_form_object ||=
  91. Publish::Allocations::EditInitial::RequestTypeForm.new(permitted_params)
  92. end
  93. 1 def number_of_places_form_object
  94. 24 permitted_params = params
  95. .slice(:number_of_places)
  96. .permit(:number_of_places)
  97. 24 @number_of_places_form_object ||= Publish::Allocations::EditInitial::NumberOfPlacesForm.new(permitted_params)
  98. end
  99. end

app/flows/initial_request_flow.rb

97.96% lines covered

98 relevant lines. 96 lines covered and 2 lines missed.
    
  1. 1 class InitialRequestFlow
  2. 1 include Rails.application.routes.url_helpers
  3. 1 PE_SUBJECT_CODE = "C6".freeze
  4. 1 attr_reader :params
  5. 1 def initialize(params:)
  6. 35 @params = params
  7. end
  8. # rubocop:disable Lint/DuplicateBranch: Duplicate branch body detected
  9. 1 def template
  10. 30 if check_your_information_page?
  11. 3 "publish/providers/allocations/check_your_information"
  12. 27 elsif number_of_places_page? || number_of_places_zero?
  13. 11 "publish/providers/allocations/number_of_places"
  14. 16 elsif blank_search_query? || empty_search_results?
  15. 3 "publish/providers/allocations/initial_request"
  16. 13 elsif pick_a_provider_page?
  17. 2 "publish/providers/allocations/pick_a_provider"
  18. else
  19. 11 "publish/providers/allocations/initial_request"
  20. end
  21. end
  22. # rubocop:enable Lint/DuplicateBranch: Duplicate branch body detected
  23. # rubocop:disable Lint/DuplicateBranch: Duplicate branch body detected
  24. 1 def locals
  25. 30 if number_of_places_page? || check_your_information_page?
  26. {
  27. 14 training_provider: training_provider,
  28. form_object: form_object,
  29. }
  30. 16 elsif blank_search_query? || empty_search_results?
  31. {
  32. 3 training_providers: training_providers_without_associated,
  33. form_object: form_object,
  34. provider: provider,
  35. }
  36. 13 elsif pick_a_provider_page?
  37. {
  38. 2 training_providers: training_providers_from_query,
  39. }
  40. else
  41. {
  42. 11 training_providers: training_providers_without_associated,
  43. form_object: form_object,
  44. provider: provider,
  45. }
  46. end
  47. end
  48. # rubocop:enable Lint/DuplicateBranch: Duplicate branch body detected
  49. 1 def redirect?
  50. 11 return false if valid_number_of_places?
  51. 8 (training_provider_search? && one_search_result?) || training_provider_selected?
  52. end
  53. 1 def allocation
  54. 5 if (training_provider_search? && one_search_result?) || training_provider_selected?
  55. 5 allocations.find_by(provider_code: training_provider[:provider_code])
  56. end
  57. end
  58. 1 def redirect_path
  59. 5 if allocation
  60. edit_provider_recruitment_cycle_allocation_path(
  61. provider.provider_code,
  62. provider.recruitment_cycle_year,
  63. training_provider.provider_code,
  64. id: allocation[:id],
  65. )
  66. 5 elsif (training_provider_search? && one_search_result?) || training_provider_selected?
  67. 5 initial_request_publish_provider_recruitment_cycle_allocations_path(
  68. provider_code: provider.provider_code,
  69. recruitment_cycle_year: recruitment_cycle.year,
  70. training_provider_code: training_provider[:provider_code],
  71. )
  72. end
  73. end
  74. 1 delegate :valid?, to: :form_object
  75. 1 private
  76. 1 def valid_number_of_places?
  77. 25 params[:number_of_places] && /\A\d+\z/.match?(params[:number_of_places]) && params[:number_of_places].to_i.positive?
  78. end
  79. 1 def number_of_places_zero?
  80. 16 params[:number_of_places]&.to_i&.zero?
  81. end
  82. 1 def training_provider_selected?
  83. 121 params[:training_provider_code].present? && params[:training_provider_code] != "-1"
  84. end
  85. 1 def training_provider_search?
  86. 72 params[:training_provider_code] == "-1" && params[:training_provider_query].present? && params[:training_provider_query].size > 1
  87. end
  88. 1 def one_search_result?
  89. 11 training_providers_from_query.count == 1
  90. end
  91. 1 def form_object
  92. 47 permitted_params = params.slice(:training_provider_code, :training_provider_query, :number_of_places)
  93. .permit(:training_provider_code, :training_provider_query, :number_of_places)
  94. 47 @form_object ||= Publish::InitialRequestForm.new(permitted_params)
  95. end
  96. 1 def allocations
  97. 21 @allocations ||= Allocation.includes(:provider, :accredited_body)
  98. .current_allocations.where(accredited_body_code: provider.provider_code)
  99. end
  100. 1 def training_provider_search_service
  101. 30 API::V3::AccreditedProviderTrainingProvidersController::TrainingProviderSearch
  102. end
  103. 1 def training_providers_with_fee_paying_pe_course
  104. 20 @training_providers_with_fee_paying_pe_course ||=
  105. training_provider_search_service.new(
  106. provider: provider,
  107. params: {
  108. filter: {
  109. subjects: PE_SUBJECT_CODE, funding_type: "fee"
  110. },
  111. },
  112. ).call
  113. end
  114. 1 def all_training_providers
  115. 14 @all_training_providers ||= training_provider_search_service.new(provider: provider, params: {}).call
  116. end
  117. 1 def training_providers_with_previous_allocations
  118. 20 @training_providers_with_previous_allocations ||= allocations.map(&:provider)
  119. end
  120. 1 def associated_training_providers
  121. 20 training_providers_with_previous_allocations + training_providers_with_fee_paying_pe_course
  122. end
  123. 1 def training_providers_without_associated
  124. 14 return @training_providers_without_associated if @training_providers_without_associated
  125. 14 ids_to_reject = associated_training_providers.map(&:id)
  126. 14 @training_providers_without_associated = all_training_providers.reject do |provider|
  127. 14 ids_to_reject.include?(provider.id)
  128. end
  129. end
  130. 1 def training_providers_from_query
  131. 23 return @training_providers_from_query if @training_providers_from_query
  132. 4 query = params[:training_provider_query]
  133. 4 @training_providers_from_query ||= recruitment_cycle.providers
  134. .provider_search(query)
  135. .limit(5)
  136. end
  137. 1 def training_providers_from_query_without_associated
  138. 6 ids_to_reject = associated_training_providers.map(&:id)
  139. 6 training_providers_from_query.reject do |r|
  140. 12 ids_to_reject.include?(r.id)
  141. end
  142. end
  143. 1 def recruitment_cycle
  144. 49 cycle_year = params[:recruitment_cycle_year] || Settings.current_recruitment_cycle_year
  145. 49 @recruitment_cycle ||= RecruitmentCycle.find_by!(year: cycle_year)
  146. end
  147. 1 def provider
  148. 70 @provider ||= recruitment_cycle.providers.find_by(provider_code: params[:provider_code])
  149. end
  150. 1 def empty_search_results?
  151. 30 return @empty_search_results if @empty_search_results
  152. 28 @empty_search_results = training_provider_search? && training_providers_from_query_without_associated.empty?
  153. 28 form_object.add_no_results_error if @empty_search_results
  154. 28 @empty_search_results
  155. end
  156. 1 def pick_a_provider_page?
  157. 26 training_provider_search? && training_providers_from_query.count > 1
  158. end
  159. 1 def number_of_places_page?
  160. 57 training_provider_selected? ||
  161. 32 (params[:training_provider_query].present? && params[:training_provider_query].size > 1 && one_search_result?) || params[:change]
  162. end
  163. 1 def check_your_information_page?
  164. 46 training_provider_selected? && valid_number_of_places? && !params[:change]
  165. end
  166. 1 def training_provider
  167. 24 @training_provider ||= if params[:training_provider_code] == "-1"
  168. training_providers_from_query.first
  169. else
  170. 19 recruitment_cycle.providers.find_by(provider_code: params[:training_provider_code].upcase)
  171. end
  172. end
  173. 1 def blank_search_query?
  174. 32 params[:training_provider_code] == "-1" && params[:training_provider_query].blank?
  175. end
  176. end

app/forms/publish/about_your_organisation_form.rb

94.87% lines covered

39 relevant lines. 37 lines covered and 2 lines missed.
    
  1. 1 module Publish
  2. 1 class AboutYourOrganisationForm < BaseProviderForm
  3. 1 validates :train_with_us, presence: { message: "Enter details about training with you" }, if: :train_with_us_changed?
  4. 1 validates :train_with_disability, presence: { message: "Enter details about training with a disability" }, if: :train_with_disability_changed?
  5. 1 validates :train_with_us, words_count: { maximum: 250, message: "Reduce the word count for training with you" }
  6. 1 validates :train_with_disability, words_count: { maximum: 250, message: "Reduce the word count for training with disabilities and other needs" }
  7. 1 validate :add_enrichment_errors
  8. 1 FIELDS = %i[
  9. train_with_us
  10. train_with_disability
  11. accrediting_provider_enrichments
  12. ].freeze
  13. 1 attr_accessor(*FIELDS)
  14. 1 def accredited_bodies
  15. 23 @accredited_bodies ||= provider.accredited_bodies.map do |ab|
  16. 7 accredited_body(ab)
  17. end
  18. end
  19. 1 private
  20. 1 def train_with_us_changed?
  21. 4 changed?(:train_with_us)
  22. end
  23. 1 def train_with_disability_changed?
  24. 4 changed?(:train_with_disability)
  25. end
  26. 1 def changed?(attribute)
  27. 8 public_send(attribute) != provider.public_send(attribute)
  28. end
  29. 1 def accredited_body(provider_name:, provider_code:, description:)
  30. 7 AccreditedBody.new(
  31. provider_name: provider_name,
  32. provider_code: provider_code,
  33. description: params_description(provider_code) || description,
  34. )
  35. end
  36. 1 def params_description(provider_code)
  37. 11 params[:accredited_bodies].to_h { |i| [i[:provider_code], i[:description]] }[provider_code] if params&.dig(:accredited_bodies).present?
  38. end
  39. 1 def accrediting_provider_enrichments
  40. 7 accredited_bodies.map do |accredited_body|
  41. {
  42. 7 UcasProviderCode: accredited_body.provider_code,
  43. Description: accredited_body.description,
  44. }
  45. end
  46. end
  47. 1 def compute_fields
  48. 7 provider.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
  49. end
  50. 1 def new_attributes
  51. 7 params.except(:accredited_bodies).merge(accrediting_provider_enrichments: accrediting_provider_enrichments)
  52. end
  53. 1 def add_enrichment_errors
  54. 4 accredited_bodies&.each_with_index do |accredited_body, _index|
  55. 4 if accredited_body.invalid?
  56. errors.add :accredited_bodies, accredited_body.errors[:description].first
  57. end
  58. end
  59. end
  60. 1 class AccreditedBody
  61. 1 include ActiveModel::Model
  62. 1 validates :description, words_count: { maximum: 100, message: lambda do |object, _data|
  63. "Reduce the word count for #{object.provider_name}"
  64. end }
  65. 1 attr_accessor :provider_name, :provider_code, :description
  66. end
  67. end
  68. end

app/forms/publish/access_request_form.rb

0.0% lines covered

31 relevant lines. 0 lines covered and 31 lines missed.
    
  1. module Publish
  2. class AccessRequestForm < BaseModelForm
  3. alias_method :access_request, :model
  4. validates :first_name, :last_name, :email_address,
  5. :organisation, :reason,
  6. presence: true
  7. FIELDS = %i[
  8. first_name
  9. last_name
  10. email_address
  11. organisation
  12. reason
  13. ].freeze
  14. attr_accessor(*FIELDS, :user)
  15. def initialize(user:, params: {})
  16. @user = user
  17. super(AccessRequest.new, params: params)
  18. end
  19. private
  20. def requester_email
  21. @requester_email ||= user.email
  22. end
  23. def assign_attributes_to_model
  24. access_request.assign_attributes(fields.except(*fields_to_ignore_before_save).merge(requester_email: requester_email))
  25. access_request.add_additional_attributes(requester_email)
  26. end
  27. def compute_fields
  28. access_request.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
  29. end
  30. end
  31. end

app/forms/publish/age_range_form.rb

100.0% lines covered

52 relevant lines. 52 lines covered and 0 lines missed.
    
  1. 2 module Publish
  2. 2 class AgeRangeForm < BaseModelForm
  3. 2 alias_method :course, :model
  4. 2 include ::Courses::EditOptions::AgeRangeConcern
  5. 2 FIELDS = %i[
  6. age_range_in_years
  7. course_age_range_in_years_other_from
  8. course_age_range_in_years_other_to
  9. ].freeze
  10. 2 attr_accessor(*FIELDS)
  11. 2 def course_age_range_in_years_other_from=(value)
  12. 10 @course_age_range_in_years_other_from = if value.present?
  13. 8 value.to_i
  14. else
  15. 2 value
  16. end
  17. end
  18. 2 def course_age_range_in_years_other_to=(value)
  19. 10 @course_age_range_in_years_other_to = if value.present?
  20. 8 value.to_i
  21. else
  22. 2 value
  23. end
  24. end
  25. 2 def initialize(*args)
  26. 15 super
  27. 15 if process_custom_range?
  28. 1 self.course_age_range_in_years_other_from = extract_from_years
  29. 1 self.course_age_range_in_years_other_to = extract_to_years
  30. 1 self.age_range_in_years = "other"
  31. end
  32. end
  33. 2 validates :age_range_in_years, presence: true
  34. # Conditional validation to fix form validation
  35. 2 with_options if: :age_range_other? do
  36. 2 validates :course_age_range_in_years_other_from, numericality: {
  37. only_integer: true,
  38. allow_blank: true,
  39. greater_than_or_equal_to: 0,
  40. less_than_or_equal_to: 46,
  41. }
  42. 2 validates :course_age_range_in_years_other_to, numericality: {
  43. only_integer: true,
  44. allow_blank: true,
  45. greater_than_or_equal_to: 4,
  46. less_than_or_equal_to: 50,
  47. }
  48. 2 validate :age_range_from_and_to_missing
  49. 2 validate :age_range_from_and_to_reversed
  50. 2 validate :age_range_spans_at_least_4_years
  51. end
  52. 2 private
  53. 2 def age_range_other?
  54. 50 age_range_in_years == "other"
  55. end
  56. 2 def presets
  57. 6 course.age_range_options
  58. end
  59. 2 def compute_fields
  60. 15 course.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
  61. end
  62. 2 def process_custom_range?
  63. 15 age_range_in_years.present? && age_range_in_years != "other" && !presets.include?(age_range_in_years)
  64. end
  65. 2 def extract_from_years
  66. 1 age_range_in_years.split("_").first
  67. end
  68. 2 def extract_to_years
  69. 1 age_range_in_years.split("_").last
  70. end
  71. 2 def age_range_from_and_to_missing
  72. 8 if age_range_in_years == "other"
  73. 8 if course_age_range_in_years_other_from.blank?
  74. 1 errors.add(:course_age_range_in_years_other_from, :blank)
  75. end
  76. 8 if course_age_range_in_years_other_to.blank?
  77. 1 errors.add(:course_age_range_in_years_other_to, :blank)
  78. end
  79. end
  80. end
  81. 2 def age_range_from_and_to_reversed
  82. 8 if age_range_in_years == "other" && course_age_range_in_years_other_from.present? && course_age_range_in_years_other_to.present? && (course_age_range_in_years_other_from.to_i > course_age_range_in_years_other_to.to_i)
  83. 2 errors.add(:course_age_range_in_years_other_from, :invalid)
  84. end
  85. end
  86. 2 def age_range_spans_at_least_4_years
  87. 8 if age_range_in_years == "other" && ((course_age_range_in_years_other_to.to_i - course_age_range_in_years_other_from.to_i).abs < 4)
  88. 3 errors.add(:course_age_range_in_years_other_to, :invalid)
  89. end
  90. end
  91. end
  92. end

app/forms/publish/allocations/edit_initial/number_of_places_form.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 2 module Publish
  2. 2 module Allocations
  3. 2 module EditInitial
  4. 2 class NumberOfPlacesForm
  5. 2 include ActiveModel::Model
  6. 2 attr_accessor :number_of_places
  7. 2 validates :number_of_places,
  8. presence: { message: "You must enter a number" },
  9. numericality: { message: "You must enter a number", other_than: 0, only_integer: true }
  10. end
  11. end
  12. end
  13. end

app/forms/publish/allocations/edit_initial/request_type_form.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 module Allocations
  3. 1 module EditInitial
  4. 1 class RequestTypeForm
  5. 1 include ActiveModel::Model
  6. 1 attr_accessor :request_type
  7. 1 validates :request_type, presence: { message: "Select one option" }
  8. end
  9. end
  10. end
  11. end

app/forms/publish/authentication/magic_link_form.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 module Authentication
  3. 1 class MagicLinkForm
  4. 1 include ActiveModel::Model
  5. 1 attr_accessor :email
  6. 1 validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  7. 1 def submit
  8. 2 if valid?
  9. 1 GenerateAndSendMagicLinkService.call(user: user) if user.present?
  10. 1 true
  11. else
  12. 1 false
  13. end
  14. end
  15. 1 private
  16. 1 def user
  17. 2 @user ||= User.find_by(email: email)
  18. end
  19. end
  20. end
  21. end

app/forms/publish/base_course_form.rb

93.75% lines covered

16 relevant lines. 15 lines covered and 1 lines missed.
    
  1. 4 module Publish
  2. 4 class BaseCourseForm < BaseModelForm
  3. 4 alias_method :course, :model
  4. 4 def save!
  5. 26 if valid?
  6. 22 save_action
  7. else
  8. 4 false
  9. end
  10. end
  11. 4 private
  12. 4 def after_successful_save_action
  13. 1 NotificationService::CourseUpdated.call(course: course)
  14. end
  15. 4 def save_action
  16. 14 assign_attributes_to_model
  17. 14 if model.save!
  18. 14 after_successful_save_action
  19. 14 true
  20. else
  21. false
  22. end
  23. end
  24. end
  25. end

app/forms/publish/base_model_form.rb

96.15% lines covered

26 relevant lines. 25 lines covered and 1 lines missed.
    
  1. 4 module Publish
  2. 4 class BaseModelForm
  3. 4 include ActiveModel::Model
  4. 4 include ActiveModel::AttributeAssignment
  5. 4 include ActiveModel::Validations::Callbacks
  6. 4 attr_accessor :model, :params, :fields
  7. 4 delegate :id, :persisted?, to: :model
  8. 4 def initialize(model, params: {})
  9. 157 @model = model
  10. 157 @params = params
  11. 157 @fields = compute_fields
  12. 157 assign_attributes(fields)
  13. end
  14. 4 def save!
  15. 21 if valid?
  16. 14 assign_attributes_to_model
  17. 14 model.save!
  18. else
  19. 7 false
  20. end
  21. end
  22. 4 private
  23. 4 def assign_attributes_to_model
  24. 18 model.assign_attributes(fields.except(*fields_to_ignore_before_save))
  25. end
  26. 4 def compute_fields
  27. raise(NotImplementedError)
  28. end
  29. 4 def fields_to_ignore_before_save
  30. 16 []
  31. end
  32. 4 def new_attributes
  33. 143 params
  34. end
  35. end
  36. end

app/forms/publish/base_provider_form.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 4 module Publish
  2. 4 class BaseProviderForm < BaseModelForm
  3. 4 alias_method :provider, :model
  4. end
  5. end

app/forms/publish/course_deletion_form.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class CourseDeletionForm < BaseModelForm
  3. 1 alias_method :course, :model
  4. 1 FIELDS = %i[
  5. confirm_course_code
  6. ].freeze
  7. 1 attr_accessor(*FIELDS)
  8. 1 validate :course_code_is_correct
  9. 1 def destroy!
  10. 2 if valid?
  11. 1 course.discard!
  12. else
  13. 1 false
  14. end
  15. end
  16. 1 private
  17. 1 def compute_fields
  18. 4 course.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
  19. end
  20. 1 def course_code_is_correct
  21. 2 return if course.course_code == confirm_course_code
  22. 1 errors.add(:confirm_course_code, :invalid_code, course_code: course.course_code)
  23. end
  24. end
  25. end

app/forms/publish/course_fee_form.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class CourseFeeForm < BaseModelForm
  3. 1 alias_method :course_enrichment, :model
  4. 1 include FundingTypeFormMethods
  5. 1 FIELDS = %i[
  6. course_length
  7. course_length_other_length
  8. fee_uk_eu
  9. fee_international
  10. fee_details
  11. financial_support
  12. ].freeze
  13. 1 attr_accessor(*FIELDS)
  14. 1 validates :course_length, presence: true
  15. 1 validates :fee_uk_eu, presence: true
  16. 1 validates :fee_uk_eu,
  17. numericality: { allow_blank: true,
  18. only_integer: true,
  19. greater_than_or_equal_to: 0,
  20. less_than_or_equal_to: 100000 }
  21. 1 validates :fee_international,
  22. numericality: { allow_blank: true,
  23. only_integer: true,
  24. greater_than_or_equal_to: 0,
  25. less_than_or_equal_to: 100000 }
  26. 1 validates :fee_details, words_count: { maximum: 250, message: :too_long }
  27. 1 validates :financial_support, words_count: { maximum: 250, message: :too_long }
  28. 1 private
  29. 1 def declared_fields
  30. 10 FIELDS
  31. end
  32. end
  33. end

app/forms/publish/course_information_form.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. 3 module Publish
  2. 3 class CourseInformationForm < BaseProviderForm
  3. 3 alias_method :course_enrichment, :model
  4. 3 FIELDS = %i[
  5. about_course
  6. interview_process
  7. how_school_placements_work
  8. ].freeze
  9. 3 attr_accessor(*FIELDS)
  10. 3 delegate :recruitment_cycle_year, :provider_code, :name, to: :course
  11. 3 validates :about_course, presence: true
  12. 3 validates :about_course, words_count: { maximum: 400, message: :too_long }
  13. 3 validates :interview_process, words_count: { maximum: 250, message: :too_long }
  14. 3 validates :how_school_placements_work, presence: true
  15. 3 validates :how_school_placements_work, words_count: { maximum: 350, message: :too_long }
  16. 3 def save!
  17. 3 if valid?
  18. 2 assign_attributes_to_model
  19. 2 course_enrichment.status = :draft if course_enrichment.rolled_over?
  20. 2 course_enrichment.save!
  21. else
  22. 1 false
  23. end
  24. end
  25. 3 private
  26. 3 def compute_fields
  27. 17 course_enrichment.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
  28. end
  29. end
  30. end

app/forms/publish/course_location_form.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class CourseLocationForm < BaseCourseForm
  3. 1 FIELDS = %i[site_ids].freeze
  4. 1 attr_accessor(*FIELDS)
  5. 1 validate :no_locations_selected
  6. 1 def initialize(model, params: {})
  7. 7 @previous_site_names = model.sites.map(&:location_name)
  8. 7 super
  9. end
  10. 1 def save_action
  11. 3 model.transaction do
  12. 3 assign_attributes_to_model
  13. 3 update_site_statuses
  14. 3 after_successful_save_action
  15. 3 true
  16. end
  17. end
  18. 1 private
  19. 1 attr_reader :previous_site_names
  20. 1 def after_successful_save_action
  21. 3 return if previous_site_names == updated_site_names
  22. 2 NotificationService::CourseSitesUpdated.call(
  23. course: course,
  24. previous_site_names: previous_site_names,
  25. updated_site_names: updated_site_names,
  26. )
  27. end
  28. 1 def updated_site_names
  29. 5 @updated_site_names ||= course.sites.map(&:location_name)
  30. end
  31. 1 def update_site_statuses
  32. 3 model.site_statuses.each do |site_status|
  33. 4 site_status.update!(
  34. publish: :published,
  35. status: :running,
  36. )
  37. end
  38. end
  39. 1 def compute_fields
  40. 7 { site_ids: course.site_ids }.merge(new_attributes)
  41. end
  42. 1 def no_locations_selected
  43. 5 return if params[:site_ids].present?
  44. 2 errors.add(:site_ids, :no_locations)
  45. end
  46. end
  47. end

app/forms/publish/course_requirement_form.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class CourseRequirementForm < BaseModelForm
  3. 1 alias_method :course_enrichment, :model
  4. 1 FIELDS = %i[
  5. personal_qualities
  6. other_requirements
  7. ].freeze
  8. 1 attr_accessor(*FIELDS)
  9. 1 validates :personal_qualities, words_count: { maximum: 100, message: :too_long }
  10. 1 validates :other_requirements, words_count: { maximum: 100, message: :too_long }
  11. 1 private
  12. 1 def compute_fields
  13. 10 course_enrichment.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
  14. end
  15. end
  16. end

app/forms/publish/course_salary_form.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class CourseSalaryForm < BaseModelForm
  3. 1 alias_method :course_enrichment, :model
  4. 1 include FundingTypeFormMethods
  5. 1 FIELDS = %i[
  6. course_length
  7. course_length_other_length
  8. salary_details
  9. ].freeze
  10. 1 attr_accessor(*FIELDS)
  11. 1 validates :course_length, presence: true
  12. 1 validates :salary_details, presence: true
  13. 1 validates :salary_details, words_count: { maximum: 250, message: :too_long }
  14. 1 private
  15. 1 def declared_fields
  16. 6 FIELDS
  17. end
  18. end
  19. end

app/forms/publish/course_study_mode_form.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class CourseStudyModeForm < BaseModelForm
  3. 1 alias_method :course, :model
  4. 1 FIELDS = %i[study_mode].freeze
  5. 1 attr_accessor(*FIELDS)
  6. 1 validates :study_mode,
  7. presence: true,
  8. inclusion: { in: Course.study_modes.keys }
  9. 1 private
  10. 1 def compute_fields
  11. 4 course.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
  12. end
  13. end
  14. end

app/forms/publish/course_subjects_form.rb

95.65% lines covered

23 relevant lines. 22 lines covered and 1 lines missed.
    
  1. 2 module Publish
  2. 2 class CourseSubjectsForm < BaseCourseForm
  3. 2 alias_method :subject_ids, :params
  4. 2 def initialize(model, params: {})
  5. 6 @previous_subject_names = model.subjects.map(&:subject_name)
  6. 6 @previous_course_name = model.name
  7. 6 super
  8. end
  9. 2 private
  10. 2 attr_reader :previous_subject_names, :previous_course_name
  11. 2 def valid?
  12. 6 super && assign_subjects_service.valid?
  13. end
  14. 2 def compute_fields
  15. 6 {}
  16. end
  17. 2 def after_successful_save_action
  18. 5 return if (previous_course_name == course.name) && (previous_subject_names == course.subjects.map(&:subject_name))
  19. 4 NotificationService::CourseSubjectsUpdated.call(
  20. course: course,
  21. previous_subject_names: previous_subject_names,
  22. previous_course_name: previous_course_name,
  23. )
  24. end
  25. 2 def assign_subjects_service
  26. 11 @assign_subjects_service ||= ::Courses::AssignSubjectsService.call(course: course, subject_ids: subject_ids)
  27. end
  28. 2 def save_action
  29. 5 if assign_subjects_service.save
  30. 5 after_successful_save_action
  31. 5 true
  32. else
  33. false
  34. end
  35. end
  36. end
  37. end

app/forms/publish/course_vacancies_form.rb

100.0% lines covered

50 relevant lines. 50 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class CourseVacanciesForm < BaseCourseForm
  3. 1 FIELDS = %i[
  4. site_statuses
  5. change_vacancies_confirmation
  6. has_vacancies
  7. ].freeze
  8. 1 attr_accessor(*FIELDS)
  9. 1 delegate :recruitment_cycle_year, :provider_code, :running_site_statuses, :name, to: :course
  10. 1 validate :vacancies_confirmation
  11. 1 def initialize(*params)
  12. 32 @site_statuses = []
  13. 32 super
  14. end
  15. 1 def site_statuses_attributes=(attributes)
  16. 7 @site_statuses = attributes.values
  17. end
  18. 1 private
  19. 1 def after_successful_save_action
  20. 13 NotificationService::CourseVacanciesUpdated.call(
  21. course: course,
  22. vacancy_statuses: vacancy_statuses,
  23. )
  24. end
  25. 1 def vacancy_statuses
  26. 13 if course.has_multiple_running_sites_or_study_modes?
  27. 7 statuses_attributes.values.map do |value|
  28. {
  29. 12 id: value[:id],
  30. status: value[:vac_status],
  31. }
  32. end
  33. else
  34. [
  35. {
  36. 6 id: statuses_attributes[:id],
  37. status: statuses_attributes[:vac_status],
  38. },
  39. ]
  40. end
  41. end
  42. 1 def compute_fields
  43. 32 course
  44. .attributes
  45. .symbolize_keys
  46. .slice(*FIELDS)
  47. .merge(new_attributes)
  48. .merge(has_vacancies: course.has_vacancies?)
  49. end
  50. 1 def assign_attributes_to_model
  51. 13 model.assign_attributes(model_attributes)
  52. end
  53. 1 def model_attributes
  54. 13 fields
  55. .symbolize_keys
  56. .except(*fields_to_ignore_before_save)
  57. .deep_merge(attributes_for_site_statuses)
  58. end
  59. 1 def fields_to_ignore_before_save
  60. 13 FIELDS
  61. end
  62. 1 def attributes_for_site_statuses
  63. 13 { site_statuses_attributes: statuses_attributes }
  64. end
  65. 1 def vacancies_confirmation
  66. 17 return if change_vacancies_confirmation.present? || params[:site_statuses_attributes].present?
  67. 4 errors.add(:change_vacancies_confirmation, vacancies_confirmation_error_message)
  68. end
  69. 1 def vacancies_confirmation_error_message
  70. 4 course.has_vacancies? ? :confirm_close_application : :confirm_reopen_application
  71. end
  72. 1 def statuses_attributes
  73. 32 if course.has_multiple_running_sites_or_study_modes?
  74. 14 attributes_for_multiple_site_statuses
  75. else
  76. 18 attributes_for_single_site_status
  77. end
  78. end
  79. 1 def attributes_for_single_site_status
  80. 18 running_site_statuses.first.attributes.merge(vac_status: single_site_status_vacancy).with_indifferent_access
  81. end
  82. 1 def attributes_for_multiple_site_statuses
  83. 14 params[:site_statuses_attributes].transform_values do |site_status_params|
  84. 24 site_status_params.except(:full_time, :part_time).merge(vac_status: site_status_vacancy(site_status_params))
  85. end
  86. end
  87. 1 def single_site_status_vacancy
  88. 18 case params[:change_vacancies_confirmation]
  89. when "no_vacancies_confirmation"
  90. 6 "no_vacancies"
  91. when "has_vacancies_confirmation"
  92. 12 course.full_time? ? "full_time_vacancies" : "part_time_vacancies"
  93. end
  94. end
  95. 1 def site_status_vacancy(site_status_params)
  96. 24 return "no_vacancies" if params[:has_vacancies] == "false"
  97. 20 VacancyStatusDeterminationService.call(
  98. vacancy_status_full_time: site_status_params[:full_time],
  99. vacancy_status_part_time: site_status_params[:part_time],
  100. course: course,
  101. )
  102. end
  103. end
  104. end

app/forms/publish/course_withdrawal_form.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. 2 module Publish
  2. 2 class CourseWithdrawalForm < BaseCourseForm
  3. 2 alias_method :course, :model
  4. 2 FIELDS = %i[
  5. confirm_course_code
  6. ].freeze
  7. 2 attr_accessor(*FIELDS)
  8. 2 validate :course_code_is_correct
  9. 2 def save!
  10. 4 if valid?
  11. 3 course.withdraw
  12. 3 after_successful_save_action
  13. 3 true
  14. else
  15. 1 false
  16. end
  17. end
  18. 2 private
  19. 2 def after_successful_save_action
  20. 3 NotificationService::CourseWithdrawn.call(course: course)
  21. end
  22. 2 def compute_fields
  23. 7 course.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
  24. end
  25. 2 def course_code_is_correct
  26. 5 return if course.course_code == confirm_course_code
  27. 2 errors.add(:confirm_course_code, :invalid_code, course_code: course.course_code)
  28. end
  29. end
  30. end

app/forms/publish/degree_grade_form.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class DegreeGradeForm
  3. # TODO: Refactor to use our form object pattern
  4. 1 include ActiveModel::Model
  5. 1 attr_accessor :grade
  6. 1 validates :grade, presence: { message: "Select the minimum degree classification you require" }
  7. 1 def save(course)
  8. 3 return false unless valid?
  9. 2 course.update(degree_grade: grade)
  10. end
  11. 1 def self.build_from_course(course)
  12. 4 new(grade: course.degree_grade)
  13. end
  14. end
  15. end

app/forms/publish/degree_start_form.rb

95.45% lines covered

22 relevant lines. 21 lines covered and 1 lines missed.
    
  1. 1 module Publish
  2. 1 class DegreeStartForm
  3. # TODO: Refactor to use our form object pattern
  4. 1 include ActiveModel::Model
  5. 1 include ActiveModel::Validations::Callbacks
  6. 1 attr_accessor :degree_grade_required
  7. 1 before_validation :cast_degree_grade_required
  8. 1 validates :degree_grade_required, inclusion: { in: [true, false], message: "Select if you require a minimum degree classification" }
  9. 1 def save(course)
  10. 6 return false unless valid? && degree_grade_required_is_false?
  11. 2 course.update(degree_grade: "not_required")
  12. end
  13. 1 def build_from_course(course)
  14. 6 self.degree_grade_required = handle_degree_grade_required(course)
  15. end
  16. 1 private
  17. 1 def cast_degree_grade_required
  18. 6 self.degree_grade_required = ActiveModel::Type::Boolean.new.cast(degree_grade_required)
  19. end
  20. 1 def degree_grade_required_is_false?
  21. 5 degree_grade_required == false
  22. end
  23. 1 def handle_degree_grade_required(course)
  24. 6 if course.degree_grade == "not_required"
  25. false
  26. 6 elsif course.degree_grade.present?
  27. 5 true
  28. end
  29. end
  30. end
  31. end

app/forms/publish/funding_type_form_methods.rb

93.75% lines covered

16 relevant lines. 15 lines covered and 1 lines missed.
    
  1. 2 module Publish
  2. 2 module FundingTypeFormMethods
  3. 2 def other_course_length?
  4. 2 course_length_is_other?(course_length)
  5. end
  6. 2 private
  7. 2 def compute_fields
  8. 16 course_enrichment
  9. .attributes
  10. .symbolize_keys
  11. .slice(*declared_fields)
  12. .merge(new_attributes)
  13. .merge(**hydrate_other_course_length)
  14. .symbolize_keys
  15. end
  16. 2 def hydrate_other_course_length
  17. 16 return {} unless course_length_is_other?(course_enrichment[:course_length])
  18. {
  19. 6 course_length: "Other",
  20. course_length_other_length: course_enrichment[:course_length],
  21. }
  22. end
  23. 2 def fields_to_ignore_before_save
  24. 2 [:course_length_other_length]
  25. end
  26. 2 def course
  27. course_enrichment.course
  28. end
  29. 2 def course_length_is_other?(value)
  30. 18 value.presence && %w[OneYear TwoYears].exclude?(value)
  31. end
  32. end
  33. end

app/forms/publish/gcse_requirements_form.rb

100.0% lines covered

28 relevant lines. 28 lines covered and 0 lines missed.
    
  1. 3 module Publish
  2. 3 class GcseRequirementsForm
  3. 3 include ActiveModel::Model
  4. 3 include ActiveModel::Validations::Callbacks
  5. 3 attr_accessor :accept_pending_gcse, :accept_gcse_equivalency, :accept_english_gcse_equivalency,
  6. :accept_maths_gcse_equivalency, :accept_science_gcse_equivalency, :additional_gcse_equivalencies, :level
  7. 3 validates :accept_pending_gcse, inclusion: { in: [true, false], message: "Select if you consider candidates with pending GCSEs" }
  8. 3 validates :accept_gcse_equivalency, inclusion: { in: [true, false], message: "Select if you consider candidates with pending equivalency tests" }
  9. 15 validate :primary_or_secondary_equivalency_details_not_given, if: -> { equivalencies_not_selected? }
  10. 15 validates :additional_gcse_equivalencies, presence: { message: "Enter details about equivalency tests" }, if: -> { equivalencies_not_selected? }
  11. 3 validates :additional_gcse_equivalencies, words_count: { maximum: 200 }
  12. 3 def save(course)
  13. 6 return false unless valid?
  14. 3 set_equivalency_values_to_false unless accept_gcse_equivalency
  15. 3 course.update(
  16. accept_pending_gcse: accept_pending_gcse,
  17. accept_gcse_equivalency: accept_gcse_equivalency,
  18. accept_english_gcse_equivalency: accept_english_gcse_equivalency,
  19. accept_maths_gcse_equivalency: accept_maths_gcse_equivalency,
  20. accept_science_gcse_equivalency: accept_science_gcse_equivalency,
  21. additional_gcse_equivalencies: additional_gcse_equivalencies,
  22. )
  23. end
  24. 3 def self.build_from_course(course)
  25. 8 new(
  26. accept_pending_gcse: course.accept_pending_gcse,
  27. accept_gcse_equivalency: course.accept_gcse_equivalency,
  28. accept_english_gcse_equivalency: course.accept_english_gcse_equivalency,
  29. accept_maths_gcse_equivalency: course.accept_maths_gcse_equivalency,
  30. accept_science_gcse_equivalency: course.accept_science_gcse_equivalency,
  31. additional_gcse_equivalencies: course.additional_gcse_equivalencies,
  32. level: course.level,
  33. )
  34. end
  35. 3 private
  36. 3 def primary_or_secondary_equivalency_details_not_given
  37. 4 if level == "primary"
  38. 1 errors.add(:equivalencies, "Select if you accept equivalency tests in English, maths or science")
  39. else
  40. 3 errors.add(:equivalencies, "Select if you accept equivalency tests in English or maths")
  41. end
  42. end
  43. 3 def equivalencies_not_selected?
  44. 24 accept_gcse_equivalency.present? && accept_english_gcse_equivalency.blank? && accept_maths_gcse_equivalency.blank? && accept_science_gcse_equivalency.blank?
  45. end
  46. 3 def set_equivalency_values_to_false
  47. 1 self.accept_english_gcse_equivalency = false
  48. 1 self.accept_maths_gcse_equivalency = false
  49. 1 self.accept_science_gcse_equivalency = false
  50. 1 self.additional_gcse_equivalencies = nil
  51. end
  52. end
  53. end

app/forms/publish/initial_request_form.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class InitialRequestForm
  3. 1 include ActiveModel::Model
  4. 1 attr_accessor :training_provider_code, :training_provider_query, :number_of_places
  5. 1 validates :training_provider_code, presence: { message: "Select or search for an organisation" }
  6. 1 validates :training_provider_query, presence: { message: "You need to add some information", if: :provider_search? }
  7. 1 validates :training_provider_query, length: { minimum: 2, message: "Please enter a minimum of two characters", if: :provider_search? }
  8. 1 validate :selected_number_of_places
  9. 1 def add_no_results_error
  10. 2 errors.add(
  11. :training_provider_query,
  12. "We could not find this organisation - please check your information and try again.",
  13. )
  14. end
  15. 1 private
  16. 1 def provider_search?
  17. 52 training_provider_code == "-1"
  18. end
  19. 1 def selected_number_of_places
  20. 26 return if number_of_places.nil?
  21. 12 errors.add(:number_of_places, "You must enter a number") unless number_of_places_valid?
  22. end
  23. 1 def number_of_places_valid?
  24. 12 !number_of_places.empty? &&
  25. /\A\d+\z/.match?(number_of_places) &&
  26. number_of_places.to_i.positive?
  27. end
  28. end
  29. end

app/forms/publish/interruption/accept_terms_form.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 2 module Publish
  2. 2 module Interruption
  3. 2 class AcceptTermsForm < BaseModelForm
  4. 2 alias_method :user, :model
  5. 2 FIELDS = %i[
  6. terms_accepted
  7. ].freeze
  8. 2 attr_accessor(*FIELDS)
  9. 2 validates :terms_accepted, acceptance: true
  10. 2 private
  11. 2 def compute_fields
  12. 12 { terms_accepted: user.accepted_terms? }.merge(new_attributes).symbolize_keys
  13. end
  14. 2 def assign_attributes_to_model
  15. 2 model.assign_attributes(accept_terms_date_utc: Time.zone.now)
  16. end
  17. end
  18. end
  19. end

app/forms/publish/location_form.rb

91.3% lines covered

23 relevant lines. 21 lines covered and 2 lines missed.
    
  1. 1 module Publish
  2. 1 class LocationForm < BaseModelForm
  3. 1 FIELDS = %i[
  4. location_name
  5. urn
  6. address1
  7. address2
  8. address3
  9. address4
  10. postcode
  11. ].freeze
  12. 1 attr_accessor(*FIELDS)
  13. 1 delegate :provider, to: :site
  14. 1 delegate :provider_code, :recruitment_cycle_year, to: :provider
  15. 1 def site
  16. 18 @model
  17. end
  18. 1 validate :location_name_unique_to_provider
  19. 1 validates :location_name, presence: { message: "Enter a name" }
  20. 1 validates :address1, presence: { message: "Enter a building and street" }
  21. 1 validates :postcode, presence: { message: "Enter a postcode" }
  22. 1 validates :postcode, postcode: { message: "Postcode is not valid (for example, BN1 1AA)" }
  23. 1 validates :urn, reference_number_format: { allow_blank: true, minimum: 5, maximum: 6, message: "URN must be 5 or 6 numbers" }
  24. 1 private
  25. 1 def assign_attributes_to_site
  26. site.assign_attributes(fields.except(*fields_to_ignore_before_save))
  27. end
  28. 1 def compute_fields
  29. 4 site.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
  30. end
  31. 1 def location_name_unique_to_provider
  32. 2 sibling_sites = provider.sites - [site]
  33. 2 if location_name.in?(sibling_sites.pluck(:location_name))
  34. errors.add(:location_name, "Name is in use by another location")
  35. end
  36. end
  37. end
  38. end

app/forms/publish/notification_form.rb

94.44% lines covered

18 relevant lines. 17 lines covered and 1 lines missed.
    
  1. 1 module Publish
  2. 1 class NotificationForm < BaseModelForm
  3. 1 alias_method :user, :model
  4. 1 FIELDS = %i[
  5. explicitly_enabled
  6. ].freeze
  7. 1 attr_accessor(*FIELDS)
  8. 1 validates :explicitly_enabled, inclusion: { in: [true, false], message: "Please select one option" }
  9. 1 def save!
  10. 2 if valid?
  11. 1 user_notification_preferences.update(enable_notifications: explicitly_enabled)
  12. else
  13. 1 false
  14. end
  15. end
  16. 1 private
  17. 1 def compute_fields
  18. 4 { explicitly_enabled: preference_selected? }.merge(new_attributes).symbolize_keys
  19. end
  20. 1 def user_notification_preferences
  21. 5 @user_notification_preferences ||= UserNotificationPreferences.new(user_id: user.id)
  22. end
  23. 1 def preference_selected?
  24. 4 return if user_notification_preferences.updated_at.blank?
  25. user_notification_preferences.enabled?
  26. end
  27. end
  28. end

app/forms/publish/provider_contact_form.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class ProviderContactForm < BaseProviderForm
  3. 1 FIELDS = %i[
  4. email telephone urn website ukprn
  5. address1 address2 address3 address4 postcode region_code
  6. ].freeze
  7. 1 attr_accessor(*FIELDS)
  8. 1 delegate :recruitment_cycle_year, :provider_code, :provider_name, :lead_school?, to: :provider
  9. 1 validates :email, email_address: { message: "Enter an email address in the correct format, like name@example.com" }
  10. 1 validates :telephone, phone: { message: "Enter a valid telephone number" }
  11. 1 validates :urn, reference_number_format: { allow_blank: false, minimum: 5, maximum: 6, message: "URN must be 5 or 6 numbers" }, if: :lead_school?
  12. 1 validates :ukprn, reference_number_format: { allow_blank: false, minimum: 8, maximum: 8, message: "UKPRN must be 8 numbers" }
  13. 1 private
  14. 1 def compute_fields
  15. 4 provider.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
  16. end
  17. end
  18. end

app/forms/publish/provider_visa_form.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 2 module Publish
  2. 2 class ProviderVisaForm < BaseProviderForm
  3. 2 FIELDS = %i[
  4. can_sponsor_student_visa
  5. can_sponsor_skilled_worker_visa
  6. ].freeze
  7. 2 attr_accessor(*FIELDS)
  8. 2 validates :can_sponsor_student_visa, inclusion: { in: [true, false], message: "Select if candidates can get a sponsored Student visa" }
  9. 2 validates :can_sponsor_skilled_worker_visa, inclusion: { in: [true, false], message: "Select if candidates can get a sponsored Skilled Worker visa" }
  10. 2 private
  11. 2 def compute_fields
  12. 7 provider.attributes.symbolize_keys.slice(*FIELDS).merge(new_attributes)
  13. end
  14. end
  15. end

app/forms/publish/repeat_request_form.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 2 module Publish
  2. 2 class RepeatRequestForm
  3. 2 include ActiveModel::Model
  4. 2 attr_accessor :request_type
  5. 2 validates :request_type, presence: { message: "Select one option" }
  6. end
  7. end

app/forms/publish/subject_requirement_form.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. 1 module Publish
  2. 1 class SubjectRequirementForm
  3. # TODO: Refactor to use our form object pattern
  4. 1 include ActiveModel::Model
  5. 1 include ActiveModel::Validations::Callbacks
  6. 1 attr_accessor :additional_degree_subject_requirements, :degree_subject_requirements
  7. 1 before_validation :cast_additional_degree_subject_requirements
  8. 1 validates :additional_degree_subject_requirements, inclusion: { in: [true, false], message: "Select if you have degree subject requirements" }
  9. 4 validates :degree_subject_requirements, presence: { message: "Enter details of degree subject requirements" }, if: -> { additional_degree_subject_requirements }
  10. 1 def save(course)
  11. 3 return false unless valid?
  12. 2 course.update(
  13. additional_degree_subject_requirements: additional_degree_subject_requirements,
  14. 2 degree_subject_requirements: additional_degree_subject_requirements.present? ? degree_subject_requirements : nil,
  15. )
  16. end
  17. 1 def self.build_from_course(course)
  18. 8 new(
  19. additional_degree_subject_requirements: course.additional_degree_subject_requirements,
  20. degree_subject_requirements: course.degree_subject_requirements,
  21. )
  22. end
  23. 1 private
  24. 1 def cast_additional_degree_subject_requirements
  25. 3 self.additional_degree_subject_requirements = ActiveModel::Type::Boolean.new.cast(additional_degree_subject_requirements)
  26. end
  27. end
  28. end

app/forms/support/edit_course_form.rb

100.0% lines covered

56 relevant lines. 56 lines covered and 0 lines missed.
    
  1. 2 module Support
  2. 2 class EditCourseForm
  3. 2 include ActiveModel::Model
  4. 2 include ActiveModel::Validations
  5. 2 FIELDS = %i[
  6. course_code name
  7. ].freeze
  8. 2 attr_accessor(*FIELDS)
  9. 2 attr_accessor :start_date_day, :start_date_month, :start_date_year, :course, :applications_open_from_day, :applications_open_from_month, :applications_open_from_year, :is_send
  10. 2 validate :validate_start_date_format
  11. 2 validate :validate_applications_open_from_format
  12. 2 def initialize(course)
  13. 26 @course = course
  14. 26 super(
  15. course_code: @course.course_code,
  16. name: @course.name,
  17. start_date_day: @course.start_date&.day,
  18. start_date_month: @course.start_date&.month,
  19. start_date_year: @course.start_date&.year,
  20. applications_open_from_day: @course.applications_open_from&.day,
  21. applications_open_from_month: @course.applications_open_from&.month,
  22. applications_open_from_year: @course.applications_open_from&.year,
  23. is_send: @course.is_send,
  24. )
  25. end
  26. 2 def save
  27. 12 return false unless valid?
  28. 3 @course.save
  29. end
  30. 2 def valid?
  31. 17 super()
  32. 17 assign_attributes_to_course
  33. 17 course.valid?
  34. 17 promote_errors_from_course
  35. 17 errors.none?
  36. end
  37. 2 def start_date
  38. 70 @start_date ||= check_date(:start_date)
  39. end
  40. 2 def applications_open_from
  41. 66 @applications_open_from ||= check_date(:applications_open_from)
  42. end
  43. 2 private
  44. 2 def check_date(date_type)
  45. 46 date_args = date_array(date_type).map(&:to_i)
  46. 46 return Date.new(*date_args) if Date.valid_date?(*date_args)
  47. 14 date_struct(date_type)
  48. end
  49. 2 def date_struct(date_type)
  50. 14 Struct.new(:day, :month, :year).new(
  51. send("#{date_type}_day"),
  52. send("#{date_type}_month"),
  53. send("#{date_type}_year"),
  54. )
  55. end
  56. 2 def date_array(date_type)
  57. 80 [send("#{date_type}_year"), send("#{date_type}_month"), send("#{date_type}_day")]
  58. end
  59. 2 def assign_attributes_to_course
  60. attributes = {
  61. 17 course_code: course_code,
  62. name: name,
  63. start_date: start_date,
  64. applications_open_from: applications_open_from,
  65. is_send: send?,
  66. }
  67. 17 course.assign_attributes(attributes)
  68. end
  69. 2 def promote_errors_from_course
  70. 17 errors.merge!(course.errors)
  71. end
  72. 2 def send?
  73. 17 ActiveModel::Type::Boolean.new.cast(is_send)
  74. end
  75. 2 def validate_start_date_format
  76. 17 validate_date(:start_date)
  77. end
  78. 2 def validate_applications_open_from_format
  79. 17 validate_date(:applications_open_from)
  80. end
  81. 2 def validate_date(date_type)
  82. 34 return if date_args_blank?(date_type)
  83. 26 errors.add(date_type, "#{attribute(date_type)} format is invalid") unless valid_date?(date_type)
  84. end
  85. 2 def attribute(date_type)
  86. 8 case date_type
  87. when :applications_open_from
  88. 4 "#{date_type.to_s.humanize.capitalize} date"
  89. else
  90. 4 date_type.to_s.humanize.capitalize
  91. end
  92. end
  93. 2 def valid_date?(date_type)
  94. 26 send(date_type).is_a?(Date)
  95. end
  96. 2 def date_args_blank?(date_type)
  97. 34 date_array(date_type).any?(&:blank?)
  98. end
  99. end
  100. end

app/helpers/age_range_errors_view_helper.rb

83.33% lines covered

12 relevant lines. 10 lines covered and 2 lines missed.
    
  1. 4 module AgeRangeErrorsViewHelper
  2. 4 def expand_another_age_range?(errors)
  3. 9 (course.other_age_range? && course.age_range_in_years.present?) ||
  4. 9 (errors && (errors[:age_range_in_years_to].present? || errors[:age_range_in_years_from].present?))
  5. end
  6. 4 def age_range_from_field_value
  7. 9 if course.other_age_range? && course.age_range_in_years.present?
  8. course.age_range_in_years.split("_").first
  9. else
  10. 9 ""
  11. end
  12. end
  13. 4 def age_range_to_field_value
  14. 9 if course.other_age_range? && course.age_range_in_years.present?
  15. course.age_range_in_years.split("_").last
  16. else
  17. 9 ""
  18. end
  19. end
  20. end

app/helpers/allocation_cycle_helper.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 4 module AllocationCycleHelper
  2. 4 def current_allocation_cycle_period_text
  3. 4 "#{Settings.allocation_cycle_year} to #{Settings.allocation_cycle_year + 1}"
  4. end
  5. 4 def next_allocation_cycle_period_text
  6. 205 "#{Settings.allocation_cycle_year + 1} to #{Settings.allocation_cycle_year + 2}"
  7. end
  8. end

app/helpers/application_helper.rb

89.19% lines covered

37 relevant lines. 33 lines covered and 4 lines missed.
    
  1. 4 module ApplicationHelper
  2. 4 include Pagy::Frontend
  3. 4 def pagy_govuk_nav(pagy)
  4. 26 render "pagy/paginator", pagy: pagy
  5. end
  6. 4 def header_items(current_user)
  7. return unless current_user
  8. items = [{ name: t("header.items.sign_out"), url: sign_out_path }]
  9. items
  10. end
  11. # rubocop:disable Rails/HelperInstanceVariable
  12. # TODO: refactor enrichment_error_link method to not use an instance variable
  13. 4 def enrichment_error_link(model, field, error)
  14. 2 href = case model
  15. when :course
  16. 2 enrichment_error_url(
  17. provider_code: @provider.provider_code,
  18. course: @course,
  19. field: field.to_s,
  20. message: error,
  21. )
  22. when :provider
  23. provider_enrichment_error_url(
  24. provider: @provider,
  25. field: field.to_s,
  26. )
  27. end
  28. 2 govuk_inset_text(classes: "app-inset-text--narrow-border app-inset-text--error") do
  29. 2 govuk_link_to(error, href)
  30. end
  31. end
  32. # TODO: refactor enrichment_summary method to not use an instance variable
  33. 4 def enrichment_summary(summary_list, model, key, value, fields, truncate_value: true, action_path: nil, action_visually_hidden_text: nil)
  34. 280 action = render_action(action_path, action_visually_hidden_text)
  35. 734 if fields.select { |field| @errors&.key? field.to_sym }.any?
  36. 1 errors = fields.map { |field|
  37. 2 @errors[field.to_sym]&.map { |error| enrichment_error_link(model, field, error) }
  38. }.flatten
  39. 1 value = raw(*errors)
  40. 1 action = nil
  41. 279 elsif truncate_value
  42. 225 classes = "app-summary-list__value--truncate"
  43. end
  44. 280 if value.blank?
  45. 66 value = raw("<span class=\"app-!-colour-muted\">Empty</span>")
  46. end
  47. 280 summary_list.row(html_attributes: { data: { qa: "enrichment__#{fields.first}" } }) do |row|
  48. 560 row.key { key.html_safe }
  49. 560 row.value(classes: classes) { value }
  50. 280 if action
  51. 261 row.action(action)
  52. else
  53. 19 row.action
  54. end
  55. end
  56. end
  57. # rubocop:enable Rails/HelperInstanceVariable
  58. 4 def display_phase_banner_border?(user)
  59. 990 user && !user.admin? && user.providers.where(recruitment_cycle: RecruitmentCycle.current).one?
  60. end
  61. 4 private
  62. 4 def render_action(action_path, action_visually_hidden_text)
  63. 280 return if action_path.blank?
  64. {
  65. 261 href: action_path,
  66. visually_hidden_text: action_visually_hidden_text,
  67. }
  68. end
  69. end

app/helpers/breadcrumb_helper.rb

91.84% lines covered

49 relevant lines. 45 lines covered and 4 lines missed.
    
  1. 4 module BreadcrumbHelper
  2. 4 def render_breadcrumbs(type)
  3. 105 breadcrumbs = send("#{type}_breadcrumb")
  4. # Don't link last item in breadcrumb
  5. 105 breadcrumbs[breadcrumbs.keys.last] = nil
  6. 105 if breadcrumbs && !FeatureService.enabled?(:new_publish_navigation)
  7. 98 render GovukComponent::BreadcrumbsComponent.new(
  8. breadcrumbs: breadcrumbs,
  9. classes: "govuk-!-display-none-print",
  10. )
  11. end
  12. end
  13. # rubocop:disable Rails/HelperInstanceVariable
  14. 4 def organisations_breadcrumb
  15. 105 current_user.has_multiple_providers? ? { "Organisations" => root_path } : {}
  16. end
  17. 4 def provider_breadcrumb
  18. 105 path = publish_provider_path(code: @provider.provider_code)
  19. 105 organisations_breadcrumb.merge({ @provider.provider_name => path })
  20. end
  21. 4 def recruitment_cycle_breadcrumb
  22. 73 if @provider.rolled_over?
  23. path = publish_provider_recruitment_cycle_path(@provider.provider_code, @recruitment_cycle.year)
  24. provider_breadcrumb.merge({ @recruitment_cycle.title => path })
  25. else
  26. 73 provider_breadcrumb
  27. end
  28. end
  29. 4 def courses_breadcrumb
  30. 62 path = publish_provider_recruitment_cycle_courses_path(@provider.provider_code)
  31. 62 recruitment_cycle_breadcrumb.merge({ "Courses" => path })
  32. end
  33. 4 def course_breadcrumb
  34. 35 path = publish_provider_recruitment_cycle_course_path(@provider.provider_code, course.recruitment_cycle_year, course.course_code)
  35. 35 courses_breadcrumb.merge({ course.name_and_code => path })
  36. end
  37. 4 def sites_breadcrumb
  38. 5 path = publish_provider_recruitment_cycle_locations_path(@provider.provider_code, @recruitment_cycle.year)
  39. 5 recruitment_cycle_breadcrumb.merge({ "Locations" => path })
  40. end
  41. 4 def organisation_details_breadcrumb
  42. 6 path = details_publish_provider_recruitment_cycle_path(@provider.provider_code, @recruitment_cycle.year)
  43. 6 recruitment_cycle_breadcrumb.merge({ "About your organisation" => path })
  44. end
  45. 4 def users_breadcrumb
  46. path = details_publish_provider_recruitment_cycle_path(@provider.provider_code, @recruitment_cycle.year)
  47. recruitment_cycle_breadcrumb.merge({ "Users" => path })
  48. end
  49. 4 def edit_site_breadcrumb
  50. 1 path = edit_publish_provider_recruitment_cycle_location_path(@provider.provider_code, @recruitment_cycle.year, @site.id)
  51. 1 sites_breadcrumb.merge({ @site.location_name.dup => path })
  52. end
  53. 4 def new_site_breadcrumb
  54. 1 path = new_publish_provider_recruitment_cycle_location_path(@provider.provider_code)
  55. 1 sites_breadcrumb.merge({ "Add a location" => path })
  56. end
  57. 4 def training_providers_breadcrumb
  58. 3 path = publish_provider_recruitment_cycle_training_providers_path(@provider.provider_code, @provider.recruitment_cycle_year)
  59. 3 provider_breadcrumb.merge({ "Courses as an accredited body" => path })
  60. end
  61. 4 def training_provider_courses_breadcrumb
  62. 1 path = publish_provider_recruitment_cycle_training_provider_courses_path(@provider.provider_code, @provider.recruitment_cycle_year, @training_provider.provider_code)
  63. 1 training_providers_breadcrumb.merge({ "#{@training_provider.provider_name}’s courses" => path })
  64. end
  65. 4 def allocations_breadcrumb
  66. 25 path = publish_provider_recruitment_cycle_allocations_path(@provider.provider_code, @provider.recruitment_cycle_year)
  67. 25 provider_breadcrumb.merge({ "Request PE courses for #{next_allocation_cycle_period_text}" => path })
  68. end
  69. 4 def allocations_closed_breadcrumb
  70. 4 path = publish_provider_recruitment_cycle_allocations_path(@provider.provider_code, @provider.recruitment_cycle_year)
  71. 4 provider_breadcrumb.merge({ "PE courses for #{next_allocation_cycle_period_text}" => path })
  72. end
  73. # rubocop:enable Rails/HelperInstanceVariable
  74. end

app/helpers/layout_helper.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module LayoutHelper
  3. # Enables inheriting from and extending another layout.
  4. # Adapted from the nestive gem: https://github.com/rwz/nestive/blob/master/lib/nestive/layout_helper.rb#L84
  5. 4 def extends_layout(layout, &block)
  6. 738 layout = layout.to_s
  7. 738 layout = "layouts/#{layout}" unless layout.include?("/")
  8. 738 view_flow.get(:layout).replace(capture(&block).to_s)
  9. 738 render(template: layout)
  10. end
  11. end

app/helpers/location_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 4 module LocationHelper
  2. 4 def urn_required?(recruitment_cycle_year)
  3. 8 recruitment_cycle_year >= Site::URN_2022_REQUIREMENTS_REQUIRED_FROM
  4. end
  5. end

app/helpers/navigation_bar_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 4 module NavigationBarHelper
  2. 4 def render_navigation_bar?(provider)
  3. 43 provider && !current_page?(root_path) && provider.recruitment_cycle && !request.path.include?("support")
  4. end
  5. end

app/helpers/provider_helper.rb

73.08% lines covered

26 relevant lines. 19 lines covered and 7 lines missed.
    
  1. 4 module ProviderHelper
  2. 4 def visa_sponsorship_status(provider)
  3. 6 if !provider.declared_visa_sponsorship?
  4. visa_sponsorship_call_to_action(provider)
  5. 6 elsif provider.can_sponsor_student_visa || provider.can_sponsor_skilled_worker_visa
  6. 6 "#{visa_sponsorship_short_status(provider)} can be sponsored"
  7. else
  8. "Visas cannot be sponsored"
  9. end
  10. end
  11. 4 def visa_sponsorship_short_status(provider)
  12. 8 if !provider.declared_visa_sponsorship?
  13. visa_sponsorship_call_to_action(provider)
  14. 8 elsif provider.can_sponsor_all_visas?
  15. 1 "Student and Skilled Worker visas"
  16. 7 elsif provider.can_only_sponsor_student_visa?
  17. 4 "Student visas"
  18. 3 elsif provider.can_only_sponsor_skilled_worker_visa?
  19. 3 "Skilled Worker visas"
  20. end
  21. end
  22. # The method below can be deleted when the feature flag is removed
  23. 4 def rolled_over_and_new_nav?(provider)
  24. 38 provider.rolled_over? && FeatureService.enabled?(:new_publish_navigation)
  25. end
  26. 4 private
  27. 4 def visa_sponsorship_call_to_action(provider)
  28. govuk_inset_text(classes: "app-inset-text--narrow-border app-inset-text--important") do
  29. raw("<p class=\"govuk-heading-s app-inset-text__title\">Can you sponsor visas?</p>") +
  30. govuk_link_to(
  31. "Select if visas can be sponsored",
  32. visas_publish_provider_recruitment_cycle_path(
  33. provider.provider_code,
  34. provider.recruitment_cycle_year,
  35. ),
  36. )
  37. end
  38. end
  39. 4 def google_form_url_for(settings, email, provider)
  40. "#{settings.url}&#{{ settings.email_entry => email, settings.provider_code_entry => provider.provider_code }.to_query}"
  41. end
  42. 4 def is_current_cycle(cycle_year)
  43. Settings.current_recruitment_cycle_year == cycle_year.to_i
  44. end
  45. end

app/helpers/publish_helper.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module PublishHelper
  3. 4 def markdown(source)
  4. 14 render = Govuk::MarkdownRenderer
  5. # Options: https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
  6. # lax_spacing: HTML blocks do not require to be surrounded by an empty line as in the Markdown standard.
  7. # autolink: parse links even when they are not enclosed in <> characters
  8. 14 options = { autolink: true, lax_spacing: true }
  9. 14 markdown = Redcarpet::Markdown.new(render, options)
  10. 14 markdown.render(source).html_safe
  11. # Convert quotes to smart quotes
  12. 14 source_with_smart_quotes = smart_quotes(source)
  13. 14 markdown.render(source_with_smart_quotes).html_safe
  14. end
  15. 4 def smart_quotes(string)
  16. 30 return "" if string.blank?
  17. 30 RubyPants.new(string, [2, :dashes], ruby_pants_options).to_html
  18. end
  19. 4 private
  20. # Use characters rather than HTML entities for smart quotes this matches how
  21. # we write smart quotes in templates and allows us to use them in <title>
  22. # elements
  23. # https://github.com/jmcnevin/rubypants/blob/master/lib/rubypants.rb
  24. 4 def ruby_pants_options
  25. 30 {
  26. double_left_quote: "“",
  27. double_right_quote: "”",
  28. single_left_quote: "‘",
  29. single_right_quote: "’",
  30. ellipsis: "…",
  31. em_dash: "—",
  32. en_dash: "–",
  33. }
  34. end
  35. end

app/helpers/recruitment_cycle_helper.rb

85.71% lines covered

7 relevant lines. 6 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module RecruitmentCycleHelper
  3. 4 def current_recruitment_cycle_period_text
  4. 1 "#{Settings.current_recruitment_cycle_year} to #{Settings.current_recruitment_cycle_year + 1}"
  5. end
  6. 4 def next_recruitment_cycle_period_text
  7. 9 "#{Settings.current_recruitment_cycle_year + 1} to #{Settings.current_recruitment_cycle_year + 2}"
  8. end
  9. 4 def previous_recruitment_cycle_period_text
  10. "#{Settings.current_recruitment_cycle_year - 1} to #{Settings.current_recruitment_cycle_year}"
  11. end
  12. end

app/helpers/support/time_helper.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module Support
  3. 4 module TimeHelper
  4. 4 def gov_uk_format(time)
  5. 11 time.strftime("%-l:%M%P on %-e %B %Y")
  6. end
  7. end
  8. end

app/helpers/support_email_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module SupportEmailHelper
  3. 4 def support_email(name: nil, subject: nil, classes: nil)
  4. 222 govuk_mail_to(Settings.support_email, name, subject: subject, class: classes)
  5. end
  6. end

app/helpers/user_helper.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module UserHelper
  3. 4 def full_name(user)
  4. [user.first_name, user.last_name].join(" ")
  5. end
  6. 4 def user_details(user)
  7. 9 "#{user[:first_name]} #{user[:last_name]} <#{user[:email]}>"
  8. end
  9. end

app/helpers/vacancy_helper.rb

92.86% lines covered

14 relevant lines. 13 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module VacancyHelper
  3. 4 def vacancy_available_for_course_site_status?(course, site_status, vacancy_study_mode = nil)
  4. 40 case course.study_mode
  5. when "full_time"
  6. 16 site_status.full_time_vacancies?
  7. when "part_time"
  8. 2 site_status.part_time_vacancies?
  9. when "full_time_or_part_time"
  10. 22 vacancy_available_for_study_mode?(site_status, vacancy_study_mode)
  11. else
  12. false
  13. end
  14. end
  15. 4 private
  16. 4 def vacancy_available_for_study_mode?(site_status, vacancy_study_mode)
  17. 22 return true if site_status.both_full_time_and_part_time_vacancies?
  18. 15 case vacancy_study_mode
  19. when :part_time
  20. 6 site_status.part_time_vacancies?
  21. when :full_time
  22. 6 site_status.full_time_vacancies?
  23. else
  24. 3 false
  25. end
  26. end
  27. end

app/helpers/view_helper.rb

97.14% lines covered

35 relevant lines. 34 lines covered and 1 lines missed.
    
  1. 4 module ViewHelper
  2. 4 def govuk_back_link_to(url = :back, body = "Back")
  3. 249 render GovukComponent::BackLinkComponent.new(
  4. text: body,
  5. href: url,
  6. classes: "govuk-!-display-none-print",
  7. html_attributes: {
  8. data: {
  9. qa: "page-back",
  10. },
  11. },
  12. )
  13. end
  14. 4 def search_ui_url(relative_path)
  15. 17 URI.join(Settings.search_ui.base_url, relative_path).to_s
  16. end
  17. 4 def search_ui_course_page_url(provider_code:, course_code:)
  18. 17 search_ui_url("/course/#{provider_code}/#{course_code}")
  19. end
  20. 4 def bat_contact_email_address
  21. 2481 Settings.support_email
  22. end
  23. 4 def bat_contact_email_address_with_wrap
  24. # https://developer.mozilla.org/en-US/docs/Web/HTML/Element/wbr
  25. # The <wbr> element will not be copied when copying and pasting the email address
  26. 1238 bat_contact_email_address.gsub("@", "<wbr>@").html_safe
  27. end
  28. 4 def bat_contact_mail_to(name = nil, **kwargs)
  29. 1243 govuk_mail_to bat_contact_email_address, name || bat_contact_email_address_with_wrap, **kwargs
  30. end
  31. # def feedback_link_to
  32. # t("feedback.link")
  33. # end
  34. 4 def title_with_error_prefix(title, error)
  35. 286 "#{t('page_titles.error_prefix') if error}#{title}"
  36. end
  37. 4 def enrichment_error_url(provider_code:, course:, field:, message: nil)
  38. 5 base = "/publish/organisations/#{provider_code}/#{course.recruitment_cycle_year}/courses/#{course.course_code}"
  39. 5 if field.to_sym == :base
  40. 2 base_errors_hash(provider_code, course)[message]
  41. else
  42. {
  43. 3 about_course: "#{base}/about?display_errors=true#publish-course-information-form-about-course-field-error",
  44. how_school_placements_work: "#{base}/about?display_errors=true#publish-course-information-form-how-school-placements-work-field-error",
  45. fee_uk_eu: "#{base}/fees?display_errors=true#fee_uk_eu-error",
  46. 3 course_length: "#{base + (course.is_fee_based? ? '/fees' : '/salary')}?display_errors=true#course_length-error",
  47. salary_details: "#{base}/salary?display_errors=true#salary_details-error",
  48. required_qualifications: "#{base}/requirements?display_errors=true#required_qualifications_wrapper",
  49. age_range_in_years: "#{base}/age-range?display_errors=true",
  50. }.with_indifferent_access[field]
  51. end
  52. end
  53. 4 def provider_enrichment_error_url(provider:, field:)
  54. 1 base = "/publish/organisations/#{provider.provider_code}/#{provider.recruitment_cycle_year}"
  55. {
  56. 1 "train_with_us" => "#{base}/about?display_errors=true#provider_train_with_us",
  57. "train_with_disability" => "#{base}/about?display_errors=true#provider_train_with_disability",
  58. "email" => "#{base}/contact?display_errors=true#provider_email",
  59. "website" => "#{base}/contact?display_errors=true#provider_website",
  60. "telephone" => "#{base}/contact?display_errors=true#provider_telephone",
  61. "address1" => "#{base}/contact?display_errors=true#provider_address1",
  62. "address3" => "#{base}/contact?display_errors=true#provider_address3",
  63. "address4" => "#{base}/contact?display_errors=true#provider_address4",
  64. "postcode" => "#{base}/contact?display_errors=true#provider_postcode",
  65. }[field]
  66. end
  67. # def environment_colour
  68. # return "purple" if sandbox_mode?
  69. # {
  70. # "development" => "grey",
  71. # "qa" => "orange",
  72. # "review" => "purple",
  73. # "rollover" => "turquoise",
  74. # "staging" => "red",
  75. # "unknown-environment" => "yellow",
  76. # }[Settings.environment.selector_name]
  77. # end
  78. # def environment_label
  79. # Settings.environment.label
  80. # end
  81. # def environment_header_class
  82. # "app-header--#{Settings.environment.selector_name}"
  83. # end
  84. # def sandbox_mode?
  85. # Settings.environment.selector_name == "sandbox"
  86. # end
  87. ## Ad-hoc, informally specified, and bug-ridden Ruby implementation of half
  88. ## of https://github.com/JedWatson/classnames.
  89. ##
  90. ## Example usage:
  91. ## <input class="<%= cns("govuk-input", "govuk-input--width-10": is_small) %>">
  92. 4 def classnames(*args)
  93. 22 args.reduce("") do |str, arg|
  94. classes =
  95. 44 case arg
  96. when Hash
  97. 44 arg.reduce([]) { |cs, (classname, condition)| cs + [condition ? classname : nil] }
  98. when String
  99. 22 [arg]
  100. else
  101. []
  102. end
  103. 44 ([str] + classes).compact_blank.join(" ")
  104. end
  105. end
  106. 4 alias_method :cns, :classnames
  107. 4 private
  108. 4 def base_errors_hash(provider_code, course)
  109. {
  110. 2 "Select if visas can be sponsored" =>
  111. visas_publish_provider_recruitment_cycle_path(provider_code, course.recruitment_cycle_year),
  112. "You must provide a Unique Reference Number (URN) for all course locations" =>
  113. locations_publish_provider_recruitment_cycle_course_path(provider_code, course.recruitment_cycle_year, course.course_code),
  114. "Enter a Unique Reference Number (URN) for all course locations" =>
  115. locations_publish_provider_recruitment_cycle_course_path(provider_code, course.recruitment_cycle_year, course.course_code),
  116. "You must provide a UK provider reference number (UKPRN)" =>
  117. contact_publish_provider_recruitment_cycle_path(provider_code, course.recruitment_cycle_year),
  118. "You must provide a UK provider reference number (UKPRN) and URN" =>
  119. contact_publish_provider_recruitment_cycle_path(provider_code, course.recruitment_cycle_year),
  120. "Enter a UK Provider Reference Number (UKPRN)" =>
  121. contact_publish_provider_recruitment_cycle_path(provider_code, course.recruitment_cycle_year),
  122. "Enter a UK Provider Reference Number (UKPRN) and URN" =>
  123. contact_publish_provider_recruitment_cycle_path(provider_code, course.recruitment_cycle_year),
  124. "Enter degree requirements" =>
  125. degrees_start_publish_provider_recruitment_cycle_course_path(provider_code, course.recruitment_cycle_year, course.course_code, display_errors: true),
  126. "Enter GCSE requirements" =>
  127. gcses_pending_or_equivalency_tests_publish_provider_recruitment_cycle_course_path(provider_code, course.recruitment_cycle_year, course.course_code, display_errors: true),
  128. }
  129. end
  130. end

app/jobs/application_job.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 4 class ApplicationJob < ActiveJob::Base
  2. 4 retry_on ActiveRecord::Deadlocked
  3. 4 discard_on ActiveJob::DeserializationError
  4. end

app/jobs/geocode_job.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 4 class GeocodeJob < ApplicationJob
  2. 4 queue_as :geocoding
  3. 4 def perform(klass, id)
  4. 1 RequestStore.store[:job_id] = provider_job_id
  5. 1 RequestStore.store[:job_queue] = queue_name
  6. 1 record = klass.classify.safe_constantize.find(id)
  7. 1 GeocoderService.geocode(obj: record) if record
  8. end
  9. end

app/jobs/save_statistic_job.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class SaveStatisticJob < ApplicationJob
  2. 1 queue_as :save_statistic
  3. 1 def perform
  4. 1 StatisticService.save
  5. end
  6. end

app/jobs/send_event_to_big_query_job.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 3 class SendEventToBigQueryJob < ApplicationJob
  2. 3 queue_as :low_priority
  3. 3 self.logger = ActiveSupport::TaggedLogging.new(Logger.new(IO::NULL))
  4. 3 def perform(event_json, dataset = Settings.google.bigquery.dataset, table = Settings.google.bigquery.table_name)
  5. 2 return unless FeatureService.enabled?(:send_request_data_to_bigquery)
  6. 1 bq = Google::Cloud::Bigquery.new
  7. 1 dataset = bq.dataset(dataset, skip_lookup: true)
  8. 1 bq_table = dataset.table(table, skip_lookup: true)
  9. 1 bq_table.insert([event_json])
  10. end
  11. end

app/lib/api_constraint.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 class ApiConstraint
  3. 4 def matches?(request)
  4. 5 Settings.publish_api_url&.include?(request.host)
  5. end
  6. end

app/lib/big_query/entity_event.rb

100.0% lines covered

24 relevant lines. 24 lines covered and 0 lines missed.
    
  1. 4 module BigQuery
  2. 4 class EntityEvent
  3. 4 CREATE_ENTITY_EVENT_TYPE = "create_entity".freeze
  4. 4 UPDATE_ENTITY_EVENT_TYPE = "update_entity".freeze
  5. 4 IMPORT_EVENT_TYPE = "import_entity".freeze
  6. 4 EVENT_TYPES = [CREATE_ENTITY_EVENT_TYPE, UPDATE_ENTITY_EVENT_TYPE, IMPORT_EVENT_TYPE].freeze
  7. 4 def initialize
  8. @event_hash = {
  9. 13178 environment: Rails.env,
  10. occurred_at: Time.zone.now.iso8601(6),
  11. }
  12. 13178 yield self if block_given?
  13. end
  14. 4 delegate :as_json, to: :event_hash
  15. 4 def with_type(type)
  16. 13175 raise "Invalid analytics event type" unless EVENT_TYPES.include?(type.to_s)
  17. 13174 @event_hash.merge!(
  18. event_type: type,
  19. )
  20. end
  21. 4 def with_entity_table_name(table_name)
  22. 13173 @event_hash.merge!(
  23. entity_table_name: table_name,
  24. )
  25. end
  26. 4 def with_data(hash)
  27. 13173 @event_hash.deep_merge!({
  28. data: hash_to_kv_pairs(hash),
  29. })
  30. end
  31. 4 private
  32. 4 attr_reader :event_hash
  33. 4 def hash_to_kv_pairs(hash)
  34. 13173 hash.map do |(key, value)|
  35. 195435 value = value.to_s if value.in? [true, false]
  36. 195435 value = value.to_json if value.is_a?(Hash) || value.is_a?(Array)
  37. 195435 { "key" => key, "value" => Array.wrap(value) }
  38. end
  39. end
  40. end
  41. end

app/lib/big_query/request_event.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. 2 module BigQuery
  2. 2 class RequestEvent
  3. 2 def initialize
  4. @event_hash = {
  5. 15 environment: ENV.fetch("RAILS_ENV", nil),
  6. occurred_at: Time.zone.now.iso8601,
  7. event_type: "web_request",
  8. }
  9. 15 yield self if block_given?
  10. end
  11. 2 delegate :as_json, to: :event_hash
  12. 2 def with_request_details(request)
  13. 9 event_hash.merge!(
  14. request_uuid: request.uuid,
  15. request_path: request.path,
  16. request_method: request.method,
  17. request_user_agent: request.user_agent,
  18. request_query: query_to_kv_pairs(request.query_string),
  19. request_referer: request.referer,
  20. anonymised_user_agent_and_ip: anonymised_user_agent_and_ip(request),
  21. )
  22. end
  23. 2 def with_response_details(response)
  24. 4 event_hash.merge!(
  25. response_content_type: response.content_type,
  26. response_status: response.status,
  27. )
  28. end
  29. 2 def with_user(user)
  30. 3 event_hash.merge!(
  31. user_id: user&.id,
  32. )
  33. end
  34. 2 private
  35. 2 attr_reader :event_hash
  36. 2 def query_to_kv_pairs(query_string)
  37. 9 vars = Rack::Utils.parse_query(query_string)
  38. 25 vars.map { |k, v| { "key" => k, "value" => [v].flatten } }
  39. end
  40. 2 def anonymised_user_agent_and_ip(rack_request)
  41. 9 if rack_request.remote_ip.present?
  42. 9 anonymise(rack_request.user_agent.to_s + rack_request.remote_ip.to_s)
  43. end
  44. end
  45. 2 def anonymise(text)
  46. 9 Digest::SHA2.hexdigest(text)
  47. end
  48. end
  49. end

app/lib/govuk/markdown_renderer.rb

66.67% lines covered

21 relevant lines. 14 lines covered and 7 lines missed.
    
  1. 1 module Govuk
  2. 1 class MarkdownRenderer < ::Redcarpet::Render::Safe
  3. 1 def block_html(raw_html)
  4. # No user input HTML please
  5. end
  6. 1 def raw_html(raw_html)
  7. # No user input HTML please
  8. end
  9. 1 def emphasis(text)
  10. # Disable feature
  11. end
  12. 1 def double_emphasis(text)
  13. # Disable feature
  14. end
  15. 1 def triple_emphasis(text)
  16. # Disable feature
  17. end
  18. 1 def list(content, list_type)
  19. case list_type
  20. when :ordered
  21. <<~HTML
  22. <ol class="govuk-list govuk-list--number">
  23. #{content}
  24. </ol>
  25. HTML
  26. when :unordered
  27. <<~HTML
  28. <ul class="govuk-list govuk-list--bullet">
  29. #{content}
  30. </ul>
  31. HTML
  32. end
  33. end
  34. 1 def link(link, _title, content)
  35. %(<a href="#{link}" class="govuk-link">#{content}</a>)
  36. end
  37. 1 def autolink(link, _link_type)
  38. %(<a href="#{link}" class="govuk-link">#{link}</a>)
  39. end
  40. 1 def paragraph(text)
  41. 28 %(<p class="govuk-body">#{text}</p>)
  42. end
  43. # Force all headers to <h3> to maintain semantic markup
  44. 1 def header(text, _heading_level)
  45. %(<h3 class="govuk-heading-m">#{text}</h3>)
  46. end
  47. 1 def self.render(content)
  48. Redcarpet::Markdown.new(self).render(content)
  49. end
  50. end
  51. end

app/lib/time_format.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 4 module TimeFormat
  2. # Primarily so that our incremental fetch can handle many records being
  3. # updated within the same second.
  4. #
  5. # The strftime format '%FT%T.%6NZ' is similar to the ISO8601 standard,
  6. # (equivalent to %FT%TZ) and adds micro-seconds (%6N).
  7. 4 def precise_time(time)
  8. 1 time.strftime("%FT%T.%6NZ")
  9. end
  10. 4 def written_month_year(time)
  11. 242 time.strftime("%B %Y")
  12. end
  13. 4 def short_date(time)
  14. 22 time.strftime("%d/%m/%Y")
  15. end
  16. 4 def gov_uk_format(time)
  17. 75 time.strftime("%-l:%M%P on %-e %B %Y")
  18. end
  19. end

app/mailers/application_mailer.rb

0.0% lines covered

4 relevant lines. 0 lines covered and 4 lines missed.
    
  1. class ApplicationMailer < ActionMailer::Base
  2. default from: "from@example.com"
  3. layout "mailer"
  4. end

app/mailers/course_publish_email_mailer.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 class CoursePublishEmailMailer < GovukNotifyRails::Mailer
  2. 1 include TimeFormat
  3. 1 def course_publish_email(course, user)
  4. 9 set_template(Settings.govuk_notify.course_publish_email_template_id)
  5. 9 set_personalisation(
  6. provider_name: course.provider.provider_name,
  7. course_name: course.name,
  8. course_code: course.course_code,
  9. course_description: course.description,
  10. course_funding_type: course.funding_type,
  11. create_course_datetime: gov_uk_format(course.created_at),
  12. course_url: create_course_url(course),
  13. )
  14. 9 mail(to: user.email)
  15. end
  16. 1 private
  17. 1 def create_course_url(course)
  18. 9 "#{Settings.find_url}" \
  19. "/course/#{course.provider.provider_code}" \
  20. "/#{course.course_code}"
  21. end
  22. end

app/mailers/course_sites_update_email_mailer.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 2 class CourseSitesUpdateEmailMailer < GovukNotifyRails::Mailer
  2. 2 include TimeFormat
  3. 2 def course_sites_update_email(
  4. course:,
  5. previous_site_names:,
  6. updated_site_names:,
  7. recipient:
  8. )
  9. 9 set_template(Settings.govuk_notify.course_sites_update_email_template_id)
  10. 9 set_personalisation(
  11. provider_name: course.provider.provider_name,
  12. course_name: course.name,
  13. course_code: course.course_code,
  14. course_url: create_course_url(course),
  15. previous_site_names: previous_site_names.join(", "),
  16. updated_site_names: updated_site_names.join(", "),
  17. sites_updated_datetime: gov_uk_format(course.updated_at),
  18. )
  19. 9 mail(to: recipient.email)
  20. end
  21. 2 private
  22. 2 def create_course_url(course)
  23. 9 "#{Settings.find_url}" \
  24. "/course/#{course.provider.provider_code}" \
  25. "/#{course.course_code}"
  26. end
  27. end

app/mailers/course_subjects_updated_email_mailer.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 2 class CourseSubjectsUpdatedEmailMailer < GovukNotifyRails::Mailer
  2. 2 include TimeFormat
  3. 2 def course_subjects_updated_email(
  4. course:,
  5. previous_subject_names:,
  6. previous_course_name:,
  7. recipient:
  8. )
  9. 10 set_template(Settings.govuk_notify.course_subjects_updated_email_template_id)
  10. 10 set_personalisation(
  11. provider_name: course.provider.provider_name,
  12. course_code: course.course_code,
  13. subject_change_datetime: gov_uk_format(course.updated_at),
  14. course_url: create_course_url(course),
  15. previous_subjects: format(previous_subject_names),
  16. updated_subjects: format(course.subjects.map(&:subject_name)),
  17. previous_course_name: previous_course_name,
  18. updated_course_name: course.name,
  19. )
  20. 10 mail(to: recipient.email)
  21. end
  22. 2 private
  23. 2 def format(subject_names)
  24. 20 subject_names.join(", ")
  25. end
  26. 2 def create_course_url(course)
  27. 10 "#{Settings.find_url}" \
  28. "/course/#{course.provider.provider_code}" \
  29. "/#{course.course_code}"
  30. end
  31. end

app/mailers/course_update_email_mailer.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 class CourseUpdateEmailMailer < GovukNotifyRails::Mailer
  2. 1 include TimeFormat
  3. 1 def course_update_email(
  4. course:,
  5. attribute_name:,
  6. original_value:,
  7. updated_value:,
  8. recipient:
  9. )
  10. 19 set_template(Settings.govuk_notify.course_update_email_template_id)
  11. 19 set_personalisation(
  12. provider_name: course.provider.provider_name,
  13. course_name: set_course_name(course, attribute_name, original_value),
  14. course_code: course.course_code,
  15. course_description: course.description,
  16. course_funding_type: course.funding_type,
  17. attribute_changed: I18n.t("course.update_email.#{attribute_name}"),
  18. attribute_change_datetime: gov_uk_format(course.updated_at),
  19. course_url: create_course_url(course),
  20. original_value: formatter.call(name: attribute_name, value: original_value),
  21. updated_value: formatter.call(name: attribute_name, value: updated_value),
  22. )
  23. 19 mail(to: recipient.email)
  24. end
  25. 1 private
  26. 1 def create_course_url(course)
  27. 19 "#{Settings.find_url}" \
  28. "/course/#{course.provider.provider_code}" \
  29. "/#{course.course_code}"
  30. end
  31. 1 def formatter
  32. 38 CourseAttributeFormatterService
  33. end
  34. 1 def set_course_name(course, attribute_name, original)
  35. 19 return original if attribute_name == "name"
  36. 18 course.name
  37. end
  38. end

app/mailers/course_vacancies/updated_mailer.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 2 module CourseVacancies
  2. 2 class UpdatedMailer < GovukNotifyRails::Mailer
  3. 2 include TimeFormat
  4. 2 def fully_updated(course:, user:, datetime:, vacancies_filled:)
  5. 11 set_template(Settings.govuk_notify.course_vacancies_updated_email_template_id)
  6. 11 set_personalisation(
  7. provider_name: course.provider.provider_name,
  8. course_name: course.name,
  9. course_code: course.course_code,
  10. vacancies_updated_datetime: gov_uk_format(datetime),
  11. course_url: create_course_url(course),
  12. 11 vacancies_filled: vacancies_filled ? "yes" : "no",
  13. 11 vacancies_opened: vacancies_filled ? "no" : "yes",
  14. )
  15. 11 mail(to: user.email)
  16. end
  17. 2 def partially_updated(course:, user:, datetime:, vacancies_opened:, vacancies_closed:)
  18. 9 set_template(Settings.govuk_notify.course_vacancies_partially_updated_email_template_id)
  19. 9 set_personalisation(
  20. provider_name: course.provider.provider_name,
  21. course_name: course.name,
  22. course_code: course.course_code,
  23. vacancies_updated_datetime: gov_uk_format(datetime),
  24. course_url: create_course_url(course),
  25. 9 vacancies_opened: vacancies_opened ? vacancies_opened.join(", ") : "none",
  26. 9 vacancies_closed: vacancies_closed ? vacancies_closed.join(", ") : "none",
  27. )
  28. 9 mail(to: user.email)
  29. end
  30. 2 private
  31. 2 def create_course_url(course)
  32. 20 "#{Settings.find_url}" \
  33. "/course/#{course.provider.provider_code}" \
  34. "/#{course.course_code}"
  35. end
  36. end
  37. end

app/mailers/course_withdraw_email_mailer.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 2 class CourseWithdrawEmailMailer < GovukNotifyRails::Mailer
  2. 2 include TimeFormat
  3. 2 def course_withdraw_email(course, user, datetime)
  4. 6 set_template(Settings.govuk_notify.course_withdraw_email_template_id)
  5. 6 set_personalisation(
  6. provider_name: course.provider.provider_name,
  7. course_name: course.name,
  8. course_code: course.course_code,
  9. withdraw_course_datetime: gov_uk_format(datetime),
  10. )
  11. 6 mail(to: user.email)
  12. end
  13. end

app/mailers/magic_link_email_mailer.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 2 class MagicLinkEmailMailer < GovukNotifyRails::Mailer
  2. 2 def magic_link_email(user)
  3. 4 set_template(Settings.govuk_notify.magic_link_email_template_id)
  4. 4 set_personalisation(
  5. first_name: user.first_name,
  6. magic_link_url: magic_link_url_for_user(user),
  7. )
  8. 4 mail(to: user.email)
  9. end
  10. 2 private
  11. 2 def magic_link_url_for_user(user)
  12. 4 "#{Settings.base_url}/signin_with_magic_link?email=#{user.email}&token=#{user.magic_link_token}"
  13. end
  14. end

app/mailers/welcome_email_mailer.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 2 class WelcomeEmailMailer < GovukNotifyRails::Mailer
  2. 2 class MissingFirstNameError < StandardError; end
  3. 2 def send_welcome_email(first_name:, email:)
  4. # Getting visibility on the missing personalisation first_name issue
  5. 4 raise MissingFirstNameError, "You must provide a firstname personalisation." if first_name.blank?
  6. 3 set_template(Settings.govuk_notify.welcome_email_template_id)
  7. 3 set_personalisation(
  8. first_name: first_name,
  9. )
  10. 3 mail(to: email)
  11. rescue MissingFirstNameError => e
  12. 1 Sentry.capture_exception(e)
  13. end
  14. end

app/models/access_request.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 4 class AccessRequest < ApplicationRecord
  2. 4 include Discard::Model
  3. 236 default_scope -> { kept }
  4. 4 belongs_to :requester, class_name: "User"
  5. 4 enum status: %i[
  6. requested
  7. approved
  8. completed
  9. declined
  10. ].freeze
  11. 6 scope :by_request_date, -> { order(request_date_utc: :asc) }
  12. 4 def recipient
  13. 6 User.new(first_name: first_name, last_name: last_name, email: email_address)
  14. end
  15. 4 def add_additional_attributes(requester_email)
  16. 3 update(requester: User.find_by(email: requester_email),
  17. request_date_utc: Time.now.utc,
  18. status: :requested)
  19. end
  20. 4 alias_method :approve, :completed!
  21. 4 validates :first_name, :last_name, :email_address,
  22. :organisation, :reason, :requester_email,
  23. presence: true
  24. 4 audited
  25. end

app/models/accrediting_provider_enrichment.rb

85.71% lines covered

21 relevant lines. 18 lines covered and 3 lines missed.
    
  1. 4 class AccreditingProviderEnrichment
  2. 4 include ActiveModel::Validations
  3. 4 include ActiveModel::Model
  4. # Pascal cased as the original is stored like so.
  5. 4 attr_accessor :UcasProviderCode, :Description
  6. 4 validates :Description, words_count: { maximum: 100 }
  7. 4 def initialize(attrs)
  8. 117 attrs.each do |attr, value|
  9. 266 send("#{attr}=", value) unless attr == "errors"
  10. end
  11. end
  12. 4 def attributes
  13. %i[UcasProviderCode Description].inject({}) do |hash, attr|
  14. hash[attr] = send(attr)
  15. hash
  16. end
  17. end
  18. 4 class ArraySerializer
  19. 4 class << self
  20. 4 def load(json)
  21. 9322 if json.present?
  22. 127 arr = JSON.parse json
  23. 127 arr.map do |item|
  24. 117 AccreditingProviderEnrichment.new(item)
  25. end
  26. end
  27. end
  28. 4 def dump(obj)
  29. 141 obj&.to_json
  30. end
  31. end
  32. end
  33. end

app/models/allocation.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. 4 class Allocation < ApplicationRecord
  2. 4 ALLOCATION_CYCLE_YEAR = Settings.allocation_cycle_year.to_s.freeze
  3. 4 include PgSearch::Model
  4. 4 belongs_to :provider
  5. 4 belongs_to :accredited_body, class_name: "Provider"
  6. 4 belongs_to :recruitment_cycle
  7. 4 has_one :allocation_uplift
  8. 4 audited associated_with: :provider
  9. 4 validates :number_of_places, numericality: true
  10. 4 validate :accredited_body_is_an_accredited_body
  11. 4 validate :non_zero_initial_request
  12. 4 enum request_type: { initial: 0, repeat: 1, declined: 2 }
  13. 31 scope :current_allocations, -> { where(recruitment_cycle: RecruitmentCycle.find_by(year: Settings.allocation_cycle_year)) }
  14. 4 pg_search_scope :search, associated_against: {
  15. provider: %i[provider_code provider_name],
  16. accredited_body: %i[provider_code provider_name],
  17. }, using: { tsearch: { prefix: true } }
  18. 4 def previous
  19. 7 Allocation.find_by(
  20. provider_code: provider_code,
  21. accredited_body_code: accredited_body_code,
  22. recruitment_cycle: recruitment_cycle.previous,
  23. )
  24. end
  25. 4 def self.journey_mode
  26. {
  27. 50 "open" => "open",
  28. "closed" => "closed",
  29. "confirmed" => "confirmed",
  30. }.fetch(Settings.features.allocations.state, "open")
  31. end
  32. 4 private
  33. 4 def accredited_body_is_an_accredited_body
  34. 97 errors.add(:accredited_body, "must be an accredited body") unless accredited_body&.accredited_body?
  35. end
  36. 4 def non_zero_initial_request
  37. 97 errors.add(:number_of_places, "must not be zero") if request_type == "initial" && number_of_places.zero?
  38. end
  39. end

app/models/allocation_uplift.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 4 class AllocationUplift < ApplicationRecord
  2. 4 belongs_to :allocation
  3. 4 delegate :provider, :recruitment_cycle, to: :allocation
  4. 4 validates :uplifts, numericality: true
  5. end

app/models/api_spec.rb

0.0% lines covered

21 relevant lines. 0 lines covered and 21 lines missed.
    
  1. # Wrapper for our API Specs
  2. #
  3. # This concern contains methods to operate on our API specs, primarily defined
  4. # as OpenAPI specs.
  5. module APISpec
  6. extend ActiveSupport::Concern
  7. class_methods do
  8. def latest
  9. version(latest_version_number)
  10. end
  11. def version(version)
  12. new(format(openapi_file_path, version: version))
  13. end
  14. end
  15. attr_reader :openapi_spec_file
  16. def initialize(path)
  17. @openapi_spec_file = path
  18. end
  19. def to_openapi
  20. Openapi3Parser.load(to_yaml)
  21. end
  22. def to_yaml
  23. @to_yaml ||= JSON.parse(File.read(@openapi_spec_file)).to_yaml
  24. end
  25. end

app/models/api_spec/public.rb

0.0% lines covered

13 relevant lines. 0 lines covered and 13 lines missed.
    
  1. module APISpec
  2. # Define the version and location of our public spec.
  3. class Public
  4. # This concern has all the goodness used externally.
  5. include APISpec
  6. class << self
  7. def latest_version_number
  8. 1
  9. end
  10. def openapi_file_path
  11. "swagger/v%{version}/swagger.json"
  12. end
  13. end
  14. end
  15. end

app/models/application_record.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 4 class ApplicationRecord < ActiveRecord::Base
  2. 4 self.abstract_class = true
  3. 4 include EmitsEntityEvents
  4. end

app/models/concerns/changed_at.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 4 module ChangedAt
  2. 4 extend ActiveSupport::Concern
  3. 4 class_methods do
  4. 4 private
  5. # Hook into Rails' built-in mechanism to update `updated_at` by adding to
  6. # it's list of columns that get updated when an object changes (by default
  7. # this is 'updated_at' and 'updated_on'). This is simpler than using a
  8. # before/after save hook and should allow our 'changed_at' to behave in
  9. # exactly the same way as 'updated_at'.
  10. 4 def timestamp_attributes_for_update
  11. 8 super + %w[changed_at]
  12. end
  13. end
  14. end

app/models/concerns/courses/edit_options.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 module EditOptions
  3. 4 extend ActiveSupport::Concern
  4. 4 include AgeRangeConcern
  5. 4 include EntryRequirementsConcern
  6. 4 include QualificationConcern
  7. 4 include StartDateConcern
  8. 4 include StudyModeConcern
  9. 4 include IsSendConcern
  10. 4 include ApplicationsOpenConcern
  11. 4 include SubjectsConcern
  12. 4 included do
  13. # When changing edit options here be sure to update the edit_options in the
  14. # courses factory in publish-teacher-training:
  15. #
  16. # https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
  17. 4 def edit_course_options
  18. {
  19. 183 entry_requirements: entry_requirements,
  20. qualifications: qualification_options,
  21. age_range_in_years: age_range_options,
  22. start_dates: start_date_options,
  23. study_modes: study_mode_options,
  24. show_is_send: show_is_send?,
  25. show_start_date: show_start_date?,
  26. show_applications_open: show_applications_open?,
  27. subjects: potential_subjects,
  28. modern_languages: modern_languages,
  29. modern_languages_subject: modern_languages_subject,
  30. }.with_indifferent_access
  31. end
  32. end
  33. end
  34. end

app/models/concerns/courses/edit_options/age_range_concern.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 module EditOptions
  3. 4 module AgeRangeConcern
  4. 4 extend ActiveSupport::Concern
  5. 4 included do
  6. # When changing anything here be sure to update the edit_options in the
  7. # courses factory in publish-teacher-training:
  8. #
  9. # https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
  10. 8 def age_range_options
  11. 197 case level
  12. when "primary"
  13. %w[
  14. 122 3_to_7
  15. 5_to_11
  16. 7_to_11
  17. 7_to_14
  18. ]
  19. when "secondary"
  20. %w[
  21. 69 11_to_16
  22. 11_to_18
  23. 14_to_19
  24. ]
  25. end
  26. end
  27. end
  28. end
  29. end
  30. end

app/models/concerns/courses/edit_options/applications_open_concern.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 module EditOptions
  3. 4 module ApplicationsOpenConcern
  4. 4 extend ActiveSupport::Concern
  5. 4 included do
  6. # When changing anything here be sure to update the edit_options in the
  7. # courses factory in publish-teacher-training:
  8. #
  9. # https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
  10. 4 def show_applications_open?
  11. 185 !is_published?
  12. end
  13. end
  14. end
  15. end
  16. end

app/models/concerns/courses/edit_options/entry_requirements_concern.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 module EditOptions
  3. 4 module EntryRequirementsConcern
  4. 4 extend ActiveSupport::Concern
  5. 4 included do
  6. # When changing anything here be sure to update the edit_options in the
  7. # courses factory in publish-teacher-training:
  8. #
  9. # https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
  10. 5 def entry_requirements
  11. 185 Course::ENTRY_REQUIREMENT_OPTIONS
  12. .keys
  13. 925 .reject { |option| option.in? %i[not_set not_required] }
  14. end
  15. end
  16. end
  17. end
  18. end

app/models/concerns/courses/edit_options/is_send_concern.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 module EditOptions
  3. 4 module IsSendConcern
  4. 4 extend ActiveSupport::Concern
  5. 4 included do
  6. # When changing anything here be sure to update the edit_options in the
  7. # courses factory in publish-teacher-training:
  8. #
  9. # https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
  10. 4 def show_is_send?
  11. 185 !is_published?
  12. end
  13. end
  14. end
  15. end
  16. end

app/models/concerns/courses/edit_options/qualification_concern.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 module EditOptions
  3. 4 module QualificationConcern
  4. 4 extend ActiveSupport::Concern
  5. 4 included do
  6. # When changing anything here be sure to update the edit_options in the
  7. # courses factory in publish-teacher-training:
  8. #
  9. # https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
  10. 6 def qualification_options
  11. 410 if level == "further_education"
  12. 15 qualifications_without_qts
  13. else
  14. 395 qualifications_with_qts
  15. end
  16. end
  17. 6 def qualifications_with_qts
  18. 395 Course.qualifications.keys.select do |qualification|
  19. 1975 qualification.include?("qts")
  20. end
  21. end
  22. 6 def qualifications_without_qts
  23. 15 Course.qualifications.keys.reject do |qualification|
  24. 75 qualification.include?("qts")
  25. end
  26. end
  27. end
  28. end
  29. end
  30. end

app/models/concerns/courses/edit_options/start_date_concern.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 module EditOptions
  3. 4 module StartDateConcern
  4. 4 extend ActiveSupport::Concern
  5. 4 included do
  6. # When changing anything here be sure to update the edit_options in the
  7. # courses factory in publish-teacher-training:
  8. #
  9. # https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
  10. 5 def start_date_options
  11. 329 recruitment_year = provider.recruitment_cycle.year.to_i
  12. 329 ["October #{recruitment_year - 1}",
  13. "November #{recruitment_year - 1}",
  14. "December #{recruitment_year - 1}",
  15. "January #{recruitment_year}",
  16. "February #{recruitment_year}",
  17. "March #{recruitment_year}",
  18. "April #{recruitment_year}",
  19. "May #{recruitment_year}",
  20. "June #{recruitment_year}",
  21. "July #{recruitment_year}",
  22. "August #{recruitment_year}",
  23. "September #{recruitment_year}",
  24. "October #{recruitment_year}",
  25. "November #{recruitment_year}",
  26. "December #{recruitment_year}",
  27. "January #{recruitment_year + 1}",
  28. "February #{recruitment_year + 1}",
  29. "March #{recruitment_year + 1}",
  30. "April #{recruitment_year + 1}",
  31. "May #{recruitment_year + 1}",
  32. "June #{recruitment_year + 1}",
  33. "July #{recruitment_year + 1}"]
  34. end
  35. 5 def show_start_date?
  36. 185 !is_published?
  37. end
  38. end
  39. end
  40. end
  41. end

app/models/concerns/courses/edit_options/study_mode_concern.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 module EditOptions
  3. 4 module StudyModeConcern
  4. 4 extend ActiveSupport::Concern
  5. 4 included do
  6. # When changing anything here be sure to update the edit_options in the
  7. # courses factory in publish-teacher-training:
  8. #
  9. # https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
  10. 4 def study_mode_options
  11. 183 Course.study_modes.keys
  12. end
  13. end
  14. end
  15. end
  16. end

app/models/concerns/courses/edit_options/subjects_concern.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 module EditOptions
  3. 4 module SubjectsConcern
  4. 4 extend ActiveSupport::Concern
  5. 4 included do
  6. # When changing anything here be sure to update the edit_options in the
  7. # courses factory in publish-teacher-training:
  8. #
  9. # https://github.com/DFE-Digital/publish-teacher-training/blob/master/spec/factories/courses.rb
  10. 4 def potential_subjects
  11. 185 assignable_master_subjects&.sort_by(&:subject_name)
  12. end
  13. 4 def modern_languages
  14. 184 ModernLanguagesSubject.all
  15. end
  16. 4 def modern_languages_subject
  17. 184 SecondarySubject.modern_languages
  18. end
  19. end
  20. end
  21. end
  22. end

app/models/concerns/emits_entity_events.rb

100.0% lines covered

26 relevant lines. 26 lines covered and 0 lines missed.
    
  1. 4 module EmitsEntityEvents
  2. 4 extend ActiveSupport::Concern
  3. 4 included do
  4. 4 attr_accessor :event_tags
  5. 4 after_create do
  6. 13019 data = entity_data(attributes)
  7. 13019 send_event(build_event(BigQuery::EntityEvent::CREATE_ENTITY_EVENT_TYPE, data)) if data.any?
  8. end
  9. 4 after_update do
  10. # in this after_update hook we don’t have access to the new fields via
  11. # #attributes — we need to dig them out of saved_changes which stores
  12. # them in the format { attr: ['old', 'new'] }
  13. 844 interesting_changes = entity_data(saved_changes.transform_values(&:last))
  14. 844 if interesting_changes.any?
  15. 634 send_event(
  16. build_event(BigQuery::EntityEvent::UPDATE_ENTITY_EVENT_TYPE, entity_data(attributes).merge(interesting_changes)),
  17. )
  18. end
  19. end
  20. end
  21. 4 def send_import_event
  22. 2 build_event(BigQuery::EntityEvent::IMPORT_EVENT_TYPE, entity_data(attributes)).tap do |event|
  23. 2 send_event(event)
  24. end
  25. end
  26. 4 private
  27. 4 def send_event(event)
  28. 13172 return unless FeatureService.enabled?(:send_request_data_to_bigquery)
  29. 5 SendEventToBigQueryJob.perform_later(event.as_json)
  30. end
  31. 4 def entity_data(changeset)
  32. 14499 exportable_attrs = Rails.configuration.analytics[self.class.table_name.to_sym]
  33. 14499 changeset.slice(*exportable_attrs&.map(&:to_s))
  34. end
  35. 4 def build_event(type, data)
  36. 13172 BigQuery::EntityEvent.new do |ee|
  37. 13172 ee.with_type(type)
  38. 13172 ee.with_entity_table_name(self.class.table_name)
  39. 13172 ee.with_data(data)
  40. end
  41. end
  42. end

app/models/concerns/postcode_normalize.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 4 module PostcodeNormalize
  2. 4 extend ActiveSupport::Concern
  3. 4 included do
  4. 4 def postcode=(str)
  5. 732 if str
  6. 729 super UKPostcode.parse(str).to_s
  7. else
  8. 3 super str
  9. end
  10. end
  11. end
  12. end

app/models/concerns/publish/course_basic_detail_concern.rb

83.33% lines covered

96 relevant lines. 80 lines covered and 16 lines missed.
    
  1. 4 module Publish
  2. 4 module CourseBasicDetailConcern
  3. 4 extend ActiveSupport::Concern
  4. 4 included do
  5. 32 decorates_assigned :course
  6. 32 before_action :build_new_course, only: %i[back new continue]
  7. 32 before_action :build_previous_course_creation_params, only: %i[new continue]
  8. 32 before_action :build_meta_course_creation_params, only: %i[new continue]
  9. 32 before_action :build_back_link, only: %i[new back continue]
  10. 32 before_action :build_course, only: %i[edit update]
  11. end
  12. 4 def back; end
  13. 4 def new
  14. 64 authorize(@provider, :can_create_course?)
  15. end
  16. 4 def edit
  17. 3 authorize(provider)
  18. end
  19. 4 def update
  20. authorize(provider)
  21. @errors = errors
  22. return render :edit if @errors.present?
  23. if @course.update(course_params)
  24. flash[:success] = I18n.t("success.saved")
  25. redirect_to(
  26. details_publish_provider_recruitment_cycle_course_path(
  27. @course.provider_code,
  28. @course.recruitment_cycle_year,
  29. @course.course_code,
  30. ),
  31. )
  32. else
  33. @errors = @course.errors.messages
  34. render :edit
  35. end
  36. end
  37. 4 def continue
  38. 45 authorize(@provider, :can_create_course?)
  39. 45 @errors = errors
  40. 45 if @errors.any?
  41. 10 render :new
  42. else
  43. 35 redirect_to next_step
  44. end
  45. end
  46. 4 private
  47. 4 def build_new_course
  48. 130 add_custom_age_range_into_params if params.dig("course", "age_range_in_years") == "other"
  49. 130 @course = ::Courses::CreationService.call(course_params: course_params, provider: provider)
  50. end
  51. 4 def build_course
  52. 26 @course = provider.courses.find_by!(course_code: params[:code])
  53. end
  54. 4 def add_custom_age_range_into_params
  55. 1 params["course"]["age_range_in_years"] = "#{age_from_param}_to_#{age_to_param}"
  56. end
  57. 4 def errors
  58. 271 @course.errors.messages.select { |key, _message| error_keys.include?(key) }
  59. end
  60. 4 def error_keys
  61. []
  62. end
  63. 4 def course_params
  64. 407 if params.key? :course
  65. 393 params.require(:course)
  66. .except(
  67. :day,
  68. :month,
  69. :year,
  70. :course_age_range_in_years_other_from,
  71. :course_age_range_in_years_other_to,
  72. :goto_confirmation,
  73. :language_ids,
  74. ).permit(
  75. policy(Course.new).permitted_new_course_attributes,
  76. sites_ids: [],
  77. subjects_ids: [],
  78. )
  79. else
  80. 14 ActionController::Parameters.new({}).permit(:course)
  81. end
  82. end
  83. 4 def build_previous_course_creation_params
  84. 123 @course_creation_params = course_params
  85. end
  86. 4 def build_meta_course_creation_params
  87. 123 @meta_course_creation_params = params.slice(:goto_confirmation)
  88. end
  89. 4 def continue_step
  90. 39 if params[:goto_confirmation] && current_step != :subjects
  91. :confirmation
  92. else
  93. 39 CourseCreationStepService.new.execute(current_step: current_step, course: @course)[:next]
  94. end
  95. end
  96. 4 def next_step
  97. 39 continue_path = course_creation_path_for(continue_step)
  98. 39 if continue_path.nil?
  99. raise "No path defined for continue step: #{continue_path}"
  100. end
  101. 39 continue_path
  102. end
  103. 4 def path_params
  104. 149 path_params = { course: course_params }
  105. 149 path_params[:goto_confirmation] = params[:goto_confirmation] if params[:goto_confirmation]
  106. 149 path_params
  107. end
  108. 4 def back_step
  109. 123 if params[:goto_confirmation]
  110. if current_step == :modern_languages
  111. :subjects
  112. else
  113. :confirmation
  114. end
  115. else
  116. 123 CourseCreationStepService.new.execute(current_step: current_step, course: @course)[:previous]
  117. end
  118. end
  119. 4 def build_back_link
  120. 123 previous_path = course_back_path_for(back_step)
  121. 123 if previous_path.nil?
  122. raise "No path defined for back step: #{back_step}"
  123. end
  124. 123 @back_link_path = previous_path
  125. end
  126. 4 def course_back_path_for(page)
  127. 123 case page
  128. when :location
  129. 7 back_publish_provider_recruitment_cycle_courses_locations_path(path_params)
  130. when :modern_languages
  131. 12 back_publish_provider_recruitment_cycle_courses_modern_languages_path(path_params)
  132. else
  133. 104 course_creation_path_for(page)
  134. end
  135. end
  136. 4 def course_creation_path_for(page)
  137. 143 case page
  138. when :courses_list
  139. 13 publish_provider_recruitment_cycle_courses_path(@provider.provider_code, @provider.recruitment_cycle_year)
  140. when :level
  141. 14 new_publish_provider_recruitment_cycle_courses_level_path(path_params)
  142. when :modern_languages
  143. 4 new_publish_provider_recruitment_cycle_courses_modern_languages_path(path_params)
  144. when :apprenticeship
  145. 5 new_publish_provider_recruitment_cycle_courses_apprenticeship_path(path_params)
  146. when :location
  147. 4 new_publish_provider_recruitment_cycle_courses_locations_path(path_params)
  148. when :entry_requirements
  149. new_publish_provider_recruitment_cycle_courses_entry_requirements_path(path_params)
  150. when :outcome
  151. 23 new_publish_provider_recruitment_cycle_courses_outcome_path(path_params)
  152. when :full_or_part_time
  153. 15 new_publish_provider_recruitment_cycle_courses_study_mode_path(path_params)
  154. when :applications_open
  155. 8 new_publish_provider_recruitment_cycle_courses_applications_open_path(path_params)
  156. when :accredited_body
  157. 8 new_publish_provider_recruitment_cycle_courses_accredited_body_path(path_params)
  158. when :start_date
  159. 3 new_publish_provider_recruitment_cycle_courses_start_date_path(path_params)
  160. when :age_range
  161. 17 new_publish_provider_recruitment_cycle_courses_age_range_path(path_params)
  162. when :subjects
  163. 13 new_publish_provider_recruitment_cycle_courses_subjects_path(path_params)
  164. when :fee_or_salary
  165. 14 new_publish_provider_recruitment_cycle_courses_fee_or_salary_path(path_params)
  166. when :confirmation
  167. 2 confirmation_publish_provider_recruitment_cycle_courses_path(path_params)
  168. end
  169. end
  170. end
  171. end

app/models/concerns/region_code.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 4 module RegionCode
  2. 4 extend ActiveSupport::Concern
  3. 4 included do
  4. # These correspond to the first-level NUTS regions for the UK (minus Northern Ireland)
  5. # https://en.wikipedia.org/wiki/First-level_NUTS_of_the_European_Union#United_Kingdom
  6. 8 enum region_code: {
  7. no_region: 0,
  8. london: 1,
  9. south_east: 2,
  10. south_west: 3,
  11. wales: 4,
  12. west_midlands: 5,
  13. east_midlands: 6,
  14. eastern: 7,
  15. north_west: 8,
  16. yorkshire_and_the_humber: 9,
  17. north_east: 10,
  18. scotland: 11,
  19. }
  20. end
  21. end

app/models/concerns/touch_course.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 4 module TouchCourse
  2. 4 extend ActiveSupport::Concern
  3. 4 included do
  4. 8 after_save :touch_course
  5. end
  6. 4 private
  7. 4 def touch_course
  8. 1088 course.update_changed_at
  9. end
  10. end

app/models/concerns/touch_provider.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 4 module TouchProvider
  2. 4 extend ActiveSupport::Concern
  3. 4 included do
  4. 12 after_save :touch_provider
  5. end
  6. 4 private
  7. 4 def touch_provider
  8. 3663 provider.update_changed_at
  9. end
  10. end

app/models/concerns/with_qualifications.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. 4 module WithQualifications
  2. 4 extend ActiveSupport::Concern
  3. 4 included do
  4. # The training programme outcome that originated in the UCAS NetUpdate system.
  5. #
  6. # See [UCAS Teacher Training Set-up Guide](https://www.ucas.com/file/115581/download?token=mv-G6P53),
  7. # page 32
  8. 4 enum profpost_flag: {
  9. # Recommendation for QTS: the student will not receive an academic teacher
  10. # training qualification on successful completion of the
  11. # programme.
  12. recommendation_for_qts: "",
  13. # Professional: the student will receive a Professional Graduate
  14. # Certificate of Education (offered at Level 6) or Professional
  15. # Graduate Diploma in Education (PGDE), with no credits or
  16. # modules at postgraduate (master's) Level 7 on successful
  17. # completion of the programme.
  18. professional: "PF",
  19. # Postgraduate: the student will receive a Postgraduate Certificate
  20. # of Education (PGCE) or other qualification which includes at
  21. # least one module or some credits at postgraduate
  22. # (master's) Level 7 on successful completion the
  23. # programme.
  24. postgraduate: "PG",
  25. # Both professional and postgraduate: the student has the option of taking at least one
  26. # postgraduate (master's) Level 7 module or obtaining some
  27. # postgraduate-level credits as part of the programme.
  28. professional_postgraduate: "BO",
  29. }
  30. # When UCAS basic courses were being imported into Manage Courses DB, this
  31. # field wasn't coming from the UCAS data. Instead, the UCAS importer derived
  32. # this field from the `profpost_flag`, the `pgde_course` table and from the
  33. # subjects this course was tagged to.
  34. #
  35. # Defined here: https://github.com/DFE-Digital/manage-courses-api/blob/master/src/ManageCourses.Domain/Models/CourseQualification.cs
  36. 4 enum qualification: { qts: 0, pgce_with_qts: 1, pgde_with_qts: 2, pgce: 3, pgde: 4 }
  37. # This field may seem like an unnecessary overhead when there is already a
  38. # database-backed `qualification` field. However it's misleading, from the
  39. # point of view of the teacher training domain, to think of 'PGCE with QTS'
  40. # as a single qualification, since the QTS and PGCE aspects are completely
  41. # separate and may even be delivered in different places by different providers.
  42. # e.g. the QTS might come from a SCITT but the PGCE would come from a university.
  43. #
  44. # It's more accurate (and hopefully more future-proof) to represent qualifications
  45. # as a list and leave the gluing of them to the presentation layer.
  46. 4 def qualifications
  47. 757 case qualification
  48. 18 when "qts" then [:qts]
  49. 709 when "pgce_with_qts" then %i[qts pgce]
  50. 16 when "pgde_with_qts" then %i[qts pgde]
  51. 8 when "pgce" then [:pgce]
  52. 4 when "pgde" then [:pgde]
  53. end
  54. end
  55. 4 def qualifications_description
  56. 324 return "" unless qualifications
  57. 322 qualifications.map(&:upcase).sort.join(" with ")
  58. end
  59. 4 def qualification=(value)
  60. 2457 super(value)
  61. 2457 self.profpost_flag = if qts?
  62. 70 :recommendation_for_qts
  63. else
  64. 2387 :postgraduate
  65. end
  66. end
  67. end
  68. end

app/models/contact.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 4 class Contact < ApplicationRecord
  2. 4 self.inheritance_column = "_unused"
  3. 4 include TouchProvider
  4. 4 belongs_to :provider
  5. 4 audited associated_with: :provider
  6. 4 validates :name, presence: true
  7. 4 validates :email, email_address: true, presence: true
  8. 4 validates :telephone, phone: true, allow_nil: true
  9. 4 validates :permission_given, acceptance: true
  10. 4 enum type: {
  11. admin: "admin",
  12. utt: "utt",
  13. web_link: "web_link",
  14. fraud: "fraud",
  15. finance: "finance",
  16. },
  17. _suffix: "contact"
  18. end

app/models/course.rb

96.64% lines covered

476 relevant lines. 460 lines covered and 16 lines missed.
    
  1. 4 class Course < ApplicationRecord
  2. 4 include Discard::Model
  3. 4 include WithQualifications
  4. 4 include ChangedAt
  5. 4 include TouchProvider
  6. 4 include Courses::EditOptions
  7. 4 include StudyModeVacancyMapper
  8. 4 include TimeFormat
  9. 4 after_initialize :set_defaults
  10. 4 before_discard do
  11. 10 raise "You cannot delete the running course #{self}" unless %i[new not_running].include?(ucas_status)
  12. end
  13. 4 has_associated_audits
  14. 4 audited
  15. 4 validates :course_code,
  16. uniqueness: { scope: :provider_id },
  17. presence: true,
  18. on: %i[create update]
  19. 4 enum program_type: {
  20. higher_education_programme: "HE",
  21. school_direct_training_programme: "SD",
  22. school_direct_salaried_training_programme: "SS",
  23. scitt_programme: "SC",
  24. pg_teaching_apprenticeship: "TA",
  25. }
  26. 4 enum study_mode: {
  27. full_time: "F",
  28. part_time: "P",
  29. full_time_or_part_time: "B",
  30. }
  31. 4 enum level: {
  32. primary: "Primary",
  33. secondary: "Secondary",
  34. further_education: "Further education",
  35. }, _suffix: :course
  36. 4 enum degree_grade: {
  37. two_one: 0,
  38. two_two: 1,
  39. third_class: 2,
  40. not_required: 9,
  41. }
  42. 4 ENTRY_REQUIREMENT_OPTIONS = {
  43. must_have_qualification_at_application_time: 1,
  44. expect_to_achieve_before_training_begins: 2,
  45. equivalence_test: 3,
  46. not_required: 9,
  47. not_set: nil,
  48. }.freeze
  49. 4 STRUCTURED_REQUIREMENTS_REQUIRED_FROM = 2022
  50. # Most providers require GCSE grade 4 ("C"),
  51. # but some require grade 5 ("strong C")
  52. 4 PROVIDERS_REQUIRING_GCSE_GRADE_5 = %w[U80 I30].freeze
  53. 4 enum maths: ENTRY_REQUIREMENT_OPTIONS, _suffix: :for_maths
  54. 4 enum english: ENTRY_REQUIREMENT_OPTIONS, _suffix: :for_english
  55. 4 enum science: ENTRY_REQUIREMENT_OPTIONS, _suffix: :for_science
  56. 4 after_validation :remove_unnecessary_enrichments_validation_message
  57. 4 before_save :set_applications_open_from
  58. 4 belongs_to :provider
  59. 4 belongs_to :accrediting_provider,
  60. 65 ->(c) { where(recruitment_cycle: c.recruitment_cycle) },
  61. class_name: "Provider",
  62. foreign_key: :accredited_body_code,
  63. primary_key: :provider_code,
  64. inverse_of: :accredited_courses,
  65. optional: true
  66. 4 has_many :course_subjects,
  67. 5869 -> { order :position },
  68. inverse_of: :course,
  69. before_add: :set_subject_position
  70. 4 delegate :recruitment_cycle, :provider_code, to: :provider, allow_nil: true
  71. 4 delegate :after_2021?, :year, to: :recruitment_cycle, allow_nil: true, prefix: :recruitment_cycle
  72. 4 def set_subject_position(course_subject)
  73. 2677 return unless course_subject.subject.secondary_subject?
  74. 323 secondary_course_subjects = course_subjects.select { |cs| cs.subject.secondary_subject? }
  75. 321 return unless secondary_course_subjects.all? { |cs| cs.position.present? }
  76. 279 course_subject.position = if secondary_course_subjects.any?
  77. 39 secondary_course_subjects.last.position + 1
  78. else
  79. 240 0
  80. end
  81. end
  82. 4 has_many :subjects, through: :course_subjects
  83. 4 has_many :financial_incentives, through: :subjects
  84. 4 has_many :site_statuses
  85. 4 accepts_nested_attributes_for :site_statuses
  86. 4 has_many :sites,
  87. 512 -> { distinct.merge(SiteStatus.where(status: %i[new_status running])) },
  88. through: :site_statuses
  89. 4 has_many :modern_languages_subjects,
  90. through: :course_subjects,
  91. source: :subject,
  92. class_name: "ModernLanguagesSubject"
  93. 4 has_many :enrichments, class_name: "CourseEnrichment" do
  94. 4 def find_or_initialize_draft
  95. # This is a ruby search as opposed to an AR search, because calling `draft`
  96. # will return a new instance of a CourseEnrichment object which is different
  97. # to the ones in the cached `enrichments` association. This makes checking
  98. # for validations later down non-trivial.
  99. 61 latest_draft_enrichment = select(&:draft?).last
  100. 61 latest_draft_enrichment || new(new_draft_attributes)
  101. end
  102. 4 def new_draft_attributes
  103. 47 latest_published_enrichment = most_recent.published.first
  104. 47 if latest_published_enrichment.present?
  105. 27 latest_published_enrichment_attributes = latest_published_enrichment
  106. .dup
  107. .attributes
  108. .with_indifferent_access
  109. .except(:json_data)
  110. 27 latest_published_enrichment_attributes[:status] = :draft
  111. 27 latest_published_enrichment_attributes
  112. else
  113. 20 { status: :draft }.with_indifferent_access
  114. end
  115. end
  116. end
  117. 120 has_one :latest_published_enrichment, -> { published.order("created_at DESC, id DESC").limit(1) },
  118. class_name: "CourseEnrichment"
  119. 4 scope :within, lambda { |range, origin:|
  120. 4 joins(site_statuses: :site).merge(SiteStatus.where(status: :running)).merge(Site.within(range, origin: origin))
  121. }
  122. 4 scope :by_name_ascending, lambda {
  123. 1 order(name: :asc)
  124. }
  125. 4 scope :by_name_descending, lambda {
  126. 1 order(name: :desc)
  127. }
  128. 4 scope :ascending_canonical_order, lambda {
  129. 10 joins(:provider).merge(Provider.by_name_ascending).order("name asc, course_code asc")
  130. }
  131. 4 scope :descending_canonical_order, lambda {
  132. 4 joins(:provider).merge(Provider.by_name_descending).order("name desc, course_code desc")
  133. }
  134. 4 scope :accredited_body_order, lambda { |provider_name|
  135. 1 joins(:provider).merge(Provider.by_provider_name(provider_name))
  136. }
  137. 4 scope :case_insensitive_search, lambda { |course_code|
  138. 1 where("lower(course.course_code) = ?", course_code.downcase)
  139. }
  140. 4 scope :changed_since, lambda { |timestamp|
  141. 17 if timestamp.present?
  142. 15 changed_at_since(timestamp)
  143. else
  144. 2 where.not(changed_at: nil)
  145. end.order(:changed_at, :id)
  146. }
  147. 4 scope :changed_at_since, lambda { |timestamp|
  148. 26 where("course.changed_at > ?", timestamp)
  149. }
  150. 4 scope :created_at_since, lambda { |timestamp|
  151. 16 where("course.created_at > ?", timestamp)
  152. }
  153. 4 scope :not_new, lambda {
  154. 3 includes(site_statuses: %i[site course])
  155. .where
  156. .not(SiteStatus.table_name => { status: SiteStatus.statuses[:new_status] })
  157. }
  158. 4 scope :published, lambda {
  159. 6 where(id: CourseEnrichment.published.select(:course_id))
  160. }
  161. 6 scope :with_recruitment_cycle, ->(year) { joins(provider: :recruitment_cycle).where(recruitment_cycle: { year: year }) }
  162. 160 scope :findable, -> { joins(:site_statuses).merge(SiteStatus.findable) }
  163. 44 scope :with_vacancies, -> { joins(:site_statuses).merge(SiteStatus.with_vacancies) }
  164. 9 scope :with_salary, -> { where(program_type: %i[school_direct_salaried_training_programme pg_teaching_apprenticeship]) }
  165. 4 scope :with_study_modes, lambda { |study_modes|
  166. 9 where(study_mode: Array(study_modes) << "full_time_or_part_time")
  167. }
  168. 4 scope :with_subjects, lambda { |subject_codes|
  169. 22 joins(:subjects).merge(Subject.with_subject_codes(subject_codes))
  170. }
  171. 4 scope :with_qualifications, lambda { |qualifications|
  172. 9 where(qualification: qualifications)
  173. }
  174. 4 scope :with_accredited_bodies, lambda { |accredited_body_codes|
  175. 1 where(accredited_body_code: accredited_body_codes)
  176. }
  177. 4 scope :with_provider_name, lambda { |provider_name|
  178. 8 where(
  179. provider_id: Provider.where(provider_name: provider_name),
  180. ).or(
  181. where(
  182. accredited_body_code: Provider.where(provider_name: provider_name)
  183. .select(:provider_code),
  184. ),
  185. )
  186. }
  187. 4 scope :with_send, lambda {
  188. 3 where(is_send: true)
  189. }
  190. 4 scope :with_funding_types, lambda { |funding_types|
  191. 23 program_types = []
  192. 23 if funding_types.include?("salary")
  193. 3 program_types << :school_direct_salaried_training_programme
  194. end
  195. 23 if funding_types.include?("apprenticeship")
  196. 3 program_types << :pg_teaching_apprenticeship
  197. end
  198. 23 if funding_types.include?("fee")
  199. 19 %i[
  200. higher_education_programme
  201. scitt_programme
  202. school_direct_training_programme
  203. ].each do |program_type|
  204. 57 program_types << program_type
  205. end
  206. end
  207. 23 where(program_type: program_types)
  208. }
  209. 4 scope :with_degree_grades, lambda { |degree_grades|
  210. 8 where(degree_grade: degree_grades)
  211. }
  212. 4 scope :provider_can_sponsor_visa, lambda {
  213. 7 joins(:provider)
  214. .where(
  215. program_type: %w[school_direct_training_programme higher_education_programme scitt_programme],
  216. provider: { can_sponsor_student_visa: true },
  217. )
  218. .or(
  219. joins(:provider)
  220. .where(
  221. program_type: %w[school_direct_salaried_training_programme pg_teaching_apprenticeship],
  222. provider: { can_sponsor_skilled_worker_visa: true },
  223. ),
  224. )
  225. }
  226. 4 def self.entry_requirement_options_without_nil_choice
  227. 72 ENTRY_REQUIREMENT_OPTIONS.reject { |option| option == :not_set }.keys.map(&:to_s)
  228. end
  229. 4 validates :maths, inclusion: { in: entry_requirement_options_without_nil_choice }, unless: lambda {
  230. 2082 further_education_course? || recruitment_cycle_after_2021?
  231. }
  232. 4 validates :english, inclusion: { in: entry_requirement_options_without_nil_choice }, unless: lambda {
  233. 2082 further_education_course? || recruitment_cycle_after_2021?
  234. }
  235. 4 validates :science, inclusion: { in: entry_requirement_options_without_nil_choice }, if: lambda {
  236. 2082 gcse_science_required? && !recruitment_cycle_after_2021?
  237. }
  238. 4 validates :is_send, inclusion: { in: [true, false] }
  239. 4 validates :sites, presence: true, on: %i[publish new]
  240. 4 validates :subjects, presence: true, on: :publish
  241. 4 validate :validate_enrichment_publishable, on: :publish
  242. 4 validate :validate_site_statuses_publishable, on: :publish
  243. 21 validate :validate_provider_visa_sponsorship_publishable, on: :publish, if: -> { recruitment_cycle_after_2021? }
  244. 21 validate :validate_provider_urn_ukprn_publishable, on: :publish, if: -> { recruitment_cycle_after_2021? }
  245. 4 validate :validate_degree_requirements_publishable, on: :publish
  246. 4 validate :validate_gcse_requirements_publishable, on: :publish
  247. 4 validate :validate_enrichment
  248. 4 validate :validate_qualification, on: %i[update new]
  249. 159 validate :validate_start_date, on: :update, if: -> { provider.present? && start_date.present? }
  250. 330 validate :validate_applications_open_from, on: %i[update new], if: -> { provider.present? }
  251. 4 validate :validate_modern_languages
  252. 4 validate :validate_has_languages, if: :has_the_modern_languages_secondary_subject_type?
  253. 4 validate :validate_subject_count
  254. 4 validate :validate_subject_consistency
  255. 1917 validate :validate_custom_age_range, on: %i[create new], if: -> { age_range_in_years.present? }
  256. 21 validate :accredited_body_exists_in_current_cycle, on: :publish, unless: -> { self_accredited? }
  257. 4 validates_with UniqueCourseValidator, on: :new
  258. 4 validates :name, :profpost_flag, :program_type, :qualification, :start_date, :study_mode, presence: true
  259. 4 validates :age_range_in_years, presence: true, on: %i[new create publish], unless: :further_education_course?
  260. 4 validates :level, presence: true, on: %i[new create publish]
  261. 4 def rollable_withdrawn?
  262. 1 content_status == :withdrawn
  263. end
  264. 4 def rollable?
  265. 33 is_published? || rollable_withdrawn?
  266. end
  267. 4 def update_notification_attributes
  268. 7 %w[name age_range_in_years qualification study_mode maths english science]
  269. end
  270. 4 def self.get_by_codes(year, provider_code, course_code)
  271. 1 RecruitmentCycle.find_by(year: year)
  272. .providers.find_by(provider_code: provider_code)
  273. .courses.find_by(course_code: course_code)
  274. end
  275. 4 def generate_name
  276. 170 services[:generate_course_name].execute(course: self)
  277. end
  278. 4 def accrediting_provider_description
  279. 206 return if accrediting_provider.blank?
  280. 71 return if provider.accrediting_provider_enrichments.blank?
  281. 5 accrediting_provider_enrichment = provider.accrediting_provider_enrichments
  282. .find do |provider|
  283. 5 provider.UcasProviderCode == accrediting_provider.provider_code
  284. end
  285. 5 accrediting_provider_enrichment.Description if accrediting_provider_enrichment.present?
  286. end
  287. 4 def publishable?
  288. 12 valid? :publish
  289. end
  290. 4 def update_valid?
  291. 8 valid? :update
  292. end
  293. 4 def findable?
  294. 982 findable_site_statuses.any?
  295. end
  296. 4 def findable_site_statuses
  297. 988 if site_statuses.loaded?
  298. 460 site_statuses.select(&:findable?)
  299. else
  300. 528 site_statuses.findable
  301. end
  302. end
  303. 4 def syncable_subjects
  304. if subjects.loaded?
  305. subjects
  306. .reject { |s| s.type == "DiscontinuedSubject" }
  307. .select { |s| s.subject_code.present? }
  308. else
  309. subjects
  310. .where.not(type: "DiscontinuedSubject")
  311. .where.not(subject_code: nil)
  312. end
  313. end
  314. 4 def open_for_applications?
  315. 233 applications_open_from.present? && applications_open_from <= Time.now.utc && findable?
  316. end
  317. 4 def has_vacancies?
  318. 279 if site_statuses.loaded?
  319. 112 site_statuses.select(&:findable?).select(&:with_vacancies?).any?
  320. else
  321. 167 site_statuses.findable.with_vacancies.any?
  322. end
  323. end
  324. 4 def has_multiple_running_sites_or_study_modes?
  325. 79 running_site_statuses.count > 1 || full_time_or_part_time?
  326. end
  327. 4 def running_site_statuses
  328. 114 site_statuses.where(status: :running)
  329. end
  330. 4 def update_changed_at(timestamp: Time.now.utc)
  331. # Changed_at represents changes to related records as well as course
  332. # itself, so we don't want to alter the semantics of updated_at which
  333. # represents changes to just the course record.
  334. 1090 update_columns changed_at: timestamp
  335. 1090 touch_provider
  336. end
  337. 4 def study_mode_description
  338. 320 study_mode.to_s.tr("_", " ")
  339. end
  340. 4 def program_type_description
  341. 317 if school_direct_salaried_training_programme? then " with salary"
  342. 306 elsif pg_teaching_apprenticeship? then " teaching apprenticeship"
  343. else
  344. 14 ""
  345. end
  346. end
  347. 4 def description
  348. 317 study_mode_string = (full_time_or_part_time? ? ", " : " ") +
  349. study_mode_description
  350. 317 qualifications_description + study_mode_string + program_type_description
  351. end
  352. 4 def content_status
  353. 1195 services[:content_status].execute(enrichment: latest_enrichment, recruitment_cycle: recruitment_cycle)
  354. end
  355. 4 def ucas_status
  356. 463 return :running if findable?
  357. 331 return :new if site_statuses.empty? || site_statuses.any?(&:status_new_status?)
  358. 25 :not_running
  359. end
  360. 4 def funding_type
  361. 5833 return if program_type.nil?
  362. 5796 if school_direct_salaried_training_programme?
  363. 209 "salary"
  364. 5587 elsif pg_teaching_apprenticeship?
  365. 5105 "apprenticeship"
  366. else
  367. 482 "fee"
  368. end
  369. end
  370. 4 def is_fee_based?
  371. 5473 funding_type == "fee"
  372. end
  373. # https://www.gov.uk/government/publications/initial-teacher-training-criteria/initial-teacher-training-itt-criteria-and-supporting-advice#c11-gcse-standard-equivalent
  374. 4 def gcse_subjects_required
  375. 2293 case level
  376. when "primary"
  377. 1911 %w[maths english science]
  378. when "secondary"
  379. 277 %w[maths english]
  380. else
  381. 105 []
  382. end
  383. end
  384. 4 def gcse_science_required?
  385. 2082 gcse_subjects_required.include?("science")
  386. end
  387. 4 def gcse_grade_required
  388. 77 if PROVIDERS_REQUIRING_GCSE_GRADE_5.any?(provider_code) || PROVIDERS_REQUIRING_GCSE_GRADE_5.any?(accrediting_provider&.provider_code)
  389. 2 5
  390. else
  391. 75 4
  392. end
  393. end
  394. 4 def last_published_at
  395. 206 latest_enrichment&.last_published_timestamp_utc
  396. end
  397. 4 def publish_sites
  398. 5 site_statuses.status_new_status.each(&:start!)
  399. 5 site_statuses.status_running.unpublished_on_ucas.each(&:published_on_ucas!)
  400. end
  401. 4 def publish_enrichment(current_user)
  402. 9 enrichments.draft.each do |enrichment|
  403. 9 enrichment.publish(current_user)
  404. end
  405. end
  406. 4 def sites=(desired_sites)
  407. 82 existing_sites = sites
  408. 82 if persisted?
  409. 17 to_add = desired_sites - existing_sites
  410. 38 to_add.each { |site| add_site!(site: site) }
  411. 17 to_remove = existing_sites - desired_sites
  412. 28 to_remove.each { |site| remove_site!(site: site) }
  413. 17 sites.reload
  414. else
  415. 65 super(desired_sites)
  416. end
  417. end
  418. 4 def has_bursary?
  419. 113 bursary_amount.present?
  420. end
  421. 4 def has_scholarship_and_bursary?
  422. 1 has_scholarship? && has_bursary?
  423. end
  424. 4 def has_scholarship?
  425. 110 scholarship_amount.present?
  426. end
  427. 4 def has_early_career_payments?
  428. 103 financial_incentive&.early_career_payments.present?
  429. end
  430. 4 def bursary_amount
  431. 219 financial_incentive&.bursary_amount
  432. end
  433. 4 def scholarship_amount
  434. 216 financial_incentive&.scholarship
  435. end
  436. 4 def financial_incentive
  437. # Ignore "modern languages" as financial incentives
  438. # differ based on the language selected
  439. 1098 subjects.reject { |subject| subject.subject_name == "Modern Languages" }.first&.financial_incentive
  440. end
  441. 4 def is_further_education?
  442. 282 level == "further_education"
  443. end
  444. 4 def degree_section_complete?
  445. 2 degree_grade.present?
  446. end
  447. 4 def is_primary?
  448. 35 level == "primary"
  449. end
  450. 4 def is_uni_or_scitt?
  451. 279 provider.accredited_body?
  452. end
  453. 4 def is_school_direct?
  454. 120 !(is_uni_or_scitt? || is_further_education?)
  455. end
  456. 4 def self_accredited?
  457. 93 provider.accredited_body?
  458. end
  459. 4 def to_s
  460. 3 "#{name} (#{provider_code}/#{course_code}) [#{recruitment_cycle}]"
  461. end
  462. 4 def next_recruitment_cycle?
  463. 2 recruitment_cycle.year > RecruitmentCycle.current_recruitment_cycle.year
  464. end
  465. # Ideally this would just use the validation, but:
  466. # https://github.com/rails/rails/issues/13971
  467. 4 def course_params_assignable(course_params, is_admin)
  468. 11 assignable_after_publish(course_params, is_admin) &&
  469. entry_requirements_assignable(course_params) &&
  470. qualification_assignable(course_params)
  471. end
  472. 4 def only_published?
  473. 49 content_status == :published
  474. end
  475. 4 def is_published?
  476. 689 %i[published published_with_unpublished_changes].include? content_status
  477. end
  478. 4 def has_unpublished_changes?
  479. 69 content_status == :published_with_unpublished_changes
  480. end
  481. 4 def is_running?
  482. 86 ucas_status == :running
  483. end
  484. 4 def is_withdrawn?
  485. 79 content_status == "withdrawn" || not_running?
  486. end
  487. 4 def not_running?
  488. 79 ucas_status == :not_running
  489. end
  490. 4 def new_and_not_running?
  491. 62 ucas_status == :new
  492. end
  493. 4 def funding_type=(funding_type)
  494. 89 assign_program_type_service = Courses::AssignProgramTypeService.new
  495. 89 assign_program_type_service.execute(funding_type, self)
  496. end
  497. 4 def ensure_site_statuses_match_study_mode
  498. site_statuses.not_no_vacancies.each do |site_status|
  499. update_vac_status(study_mode, site_status)
  500. end
  501. end
  502. 4 def withdraw
  503. 8 if is_published?
  504. 5 site_statuses.each do |site_status|
  505. 13 site_status.update(vac_status: :no_vacancies, status: :suspended)
  506. end
  507. 5 withdraw_latest_enrichment
  508. else
  509. 3 errors.add(:withdraw, "Courses that have not been published should be deleted not withdrawn")
  510. end
  511. end
  512. 4 def assignable_master_subjects
  513. 186 services[:assignable_master_subjects].execute(course: self)
  514. end
  515. 4 def assignable_subjects
  516. 1 services[:assignable_subjects].execute(course: self)
  517. end
  518. 4 def in_current_cycle?
  519. 37 recruitment_cycle.current?
  520. end
  521. 4 def age_minimum
  522. 103 return if age_range_in_years.blank?
  523. 102 age_range_in_years.split("_").first.to_i
  524. end
  525. 4 def age_maximum
  526. 103 return if age_range_in_years.blank?
  527. 102 age_range_in_years.split("_").last.to_i
  528. end
  529. 4 def bursary_requirements
  530. 104 return [] unless has_bursary?
  531. 4 requirements = [I18n.t("course.values.bursary_requirements.second_degree")]
  532. 4 mathematics_requirement = I18n.t("course.values.bursary_requirements.maths")
  533. 8 if subjects.any? { |subject| subject.subject_name == "Primary with mathematics" }
  534. requirements.push(mathematics_requirement)
  535. end
  536. 4 requirements
  537. end
  538. 4 def validate_degree_requirements_publishable
  539. 19 return true if recruitment_cycle.year.to_i < STRUCTURED_REQUIREMENTS_REQUIRED_FROM || degree_grade.present?
  540. 1 errors.add(:base, :degree_requirements_not_publishable)
  541. 1 false
  542. end
  543. 4 def validate_gcse_requirements_publishable
  544. 19 return true if recruitment_cycle.year.to_i < STRUCTURED_REQUIREMENTS_REQUIRED_FROM || !accept_pending_gcse.nil? || !accept_gcse_equivalency.nil?
  545. 14 errors.add(:base, :gcse_requirements_not_publishable)
  546. 14 false
  547. end
  548. 4 def required_qualifications
  549. 101 RequiredQualificationsSummary.new(self).extract
  550. end
  551. 4 def enrichment_attribute(enrichment_name)
  552. 235 enrichments.most_recent&.first&.public_send(enrichment_name)
  553. end
  554. 4 def remove_carat_from_error_messages
  555. 142 new_errors = errors.map do |error|
  556. 945 message = error.message.start_with?("^") ? error.message[1..] : error.message
  557. 945 [error.attribute, message]
  558. end
  559. 142 errors.clear
  560. 142 new_errors.each do |attribute, message|
  561. 945 errors.add attribute, message: message
  562. end
  563. end
  564. 4 private
  565. 4 def add_site!(site:)
  566. 21 is_course_new = ucas_status == :new
  567. 21 site_status = site_statuses.find_or_initialize_by(site: site)
  568. 21 site_status.start! unless is_course_new
  569. 21 site_status.save!
  570. end
  571. 4 def remove_site!(site:)
  572. 11 site_status = site_statuses.find_by!(site: site)
  573. 11 ucas_status == :new ? site_status.destroy! : site_status.suspend!
  574. end
  575. 4 def withdraw_latest_enrichment
  576. 5 latest_enrichment.withdraw
  577. end
  578. 4 def latest_enrichment
  579. 1406 return if enrichments.empty?
  580. 525 if enrichments.last.created_at.nil?
  581. 20 enrichments.last
  582. else
  583. 505 enrichments.max_by(&:created_at)
  584. end
  585. end
  586. 4 def assignable_after_publish(course_params, is_admin)
  587. 11 params_keys = [*(:is_send unless is_admin), :applications_open_from, :application_start_date]
  588. 11 relevant_params = course_params.slice(*params_keys)
  589. 11 return true if relevant_params.empty? || !is_published?
  590. 3 relevant_params.each do |field, _value|
  591. 3 errors.add(field.to_sym, "cannot be changed after publish")
  592. end
  593. 3 false
  594. end
  595. 4 def entry_requirements_assignable(course_params)
  596. 8 relevant_params = course_params.slice(:maths, :english, :science)
  597. 8 invalid_params = relevant_params.select do |_subject, value|
  598. 2 value && !ENTRY_REQUIREMENT_OPTIONS.key?(value.to_sym)
  599. end
  600. 8 invalid_params.each do |subject, _value|
  601. 1 errors.add(subject.to_sym, "is invalid")
  602. end
  603. 8 invalid_params.empty?
  604. end
  605. 4 def qualification_assignable(course_params)
  606. 7 assignable = course_params[:qualification].nil? || Course.qualifications.include?(course_params[:qualification].to_sym)
  607. 7 errors.add(:qualification, "is invalid") unless assignable
  608. 7 assignable
  609. end
  610. 4 def add_enrichment_errors(enrichment)
  611. 142 enrichment.errors.messages.map do |field, _error|
  612. # `full_messages_for` here will remove any `^`s defined in the validator or en.yml.
  613. # We still need it for later, so re-add it.
  614. # jsonapi_errors will throw if it's given an array, so we call `.first`.
  615. 34 message = "^#{enrichment.errors.full_messages_for(field).first}"
  616. 34 errors.add field.to_sym, message
  617. end
  618. end
  619. 4 def validate_enrichment
  620. 2082 latest_enrichment = enrichments.select(&:draft?).last
  621. 2082 return if latest_enrichment.blank?
  622. 126 latest_enrichment.valid?
  623. 126 add_enrichment_errors(latest_enrichment)
  624. end
  625. 4 def validate_enrichment_publishable
  626. 17 if enrichments.blank?
  627. 8 temp_enrichment = CourseEnrichment.new(course: self, status: "draft")
  628. 8 temp_enrichment.valid?(:publish)
  629. 8 add_enrichment_errors(temp_enrichment)
  630. else
  631. 9 latest_enrichment = enrichments.select(&:draft?).last
  632. 9 if latest_enrichment
  633. 8 latest_enrichment.valid?(:publish)
  634. 8 add_enrichment_errors(latest_enrichment)
  635. end
  636. end
  637. end
  638. 4 def validate_site_statuses_publishable
  639. 17 site_statuses.each do |site_status|
  640. 8 unless site_status.valid?
  641. raise "Site status invalid on course #{provider_code}/#{course_code}: #{site_status.errors.full_messages.first}"
  642. end
  643. end
  644. end
  645. 4 def validate_provider_visa_sponsorship_publishable
  646. 17 if provider.can_sponsor_student_visa.nil? || provider.can_sponsor_skilled_worker_visa.nil?
  647. errors.add(:base, :visa_sponsorship_not_publishable)
  648. end
  649. end
  650. 4 def validate_provider_urn_ukprn_publishable
  651. 17 if provider.lead_school? && (provider.ukprn.blank? || provider.urn.blank?)
  652. errors.add(:base, :provider_ukprn_and_urn_not_publishable)
  653. 17 elsif provider.ukprn.blank?
  654. errors.add(:base, :provider_ukprn_not_publishable)
  655. end
  656. end
  657. 4 def set_defaults
  658. 3975 self.modular ||= ""
  659. end
  660. 4 def remove_unnecessary_enrichments_validation_message
  661. 2082 errors.delete :enrichments if errors[:enrichments] == ["is invalid"]
  662. end
  663. 4 def validate_qualification
  664. 326 if qualification.blank?
  665. 107 errors.add(:qualification, :blank)
  666. else
  667. 219 errors.add(:qualification, "^#{qualifications_description} is not valid for a #{level.to_s.humanize.downcase} course") unless qualification.in?(qualification_options)
  668. end
  669. end
  670. 4 def set_applications_open_from
  671. 1631 self.applications_open_from ||= recruitment_cycle.application_start_date
  672. end
  673. 4 def validate_start_date
  674. 144 errors.add :start_date, "#{start_date.strftime('%B %Y')} is not in the #{recruitment_cycle.year} cycle" unless start_date_options.include?(written_month_year(start_date))
  675. end
  676. 4 def validate_applications_open_from
  677. 324 if applications_open_from.blank? || applications_open_from.is_a?(Struct)
  678. 141 errors.add(:applications_open_from, :blank)
  679. 183 elsif !valid_date_range.include?(applications_open_from)
  680. 7 chosen_date = short_date(applications_open_from)
  681. 7 start_date = short_date(recruitment_cycle.application_start_date)
  682. 7 end_date = short_date(recruitment_cycle.application_end_date)
  683. 7 errors.add(
  684. :applications_open_from,
  685. "#{chosen_date} is not valid for the #{provider.recruitment_cycle.year} cycle. " \
  686. "A valid date must be between #{start_date} and #{end_date}",
  687. )
  688. end
  689. end
  690. 4 def validate_modern_languages
  691. 2082 if has_any_modern_language_subject_type? && !has_the_modern_languages_secondary_subject_type?
  692. errors.add(:subjects, "Modern languages subjects must also have the modern_languages subject")
  693. end
  694. end
  695. 4 def validate_site_status_findable
  696. unless findable?
  697. errors.add(:site_statuses, "must be findable")
  698. end
  699. end
  700. 4 def has_any_modern_language_subject_type?
  701. 4172 subjects.any? { |subject| subject.type == "ModernLanguagesSubject" }
  702. end
  703. 4 def has_the_modern_languages_secondary_subject_type?
  704. 2095 raise "SecondarySubject not found" if SecondarySubject.nil?
  705. 2095 raise "SecondarySubject.modern_languages not found" if SecondarySubject.modern_languages.nil?
  706. 4125 subjects.any? { |subject| subject&.id == SecondarySubject.modern_languages.id }
  707. end
  708. 4 def validate_has_languages
  709. 24 unless has_any_modern_language_subject_type?
  710. 11 errors.add(:modern_languages_subjects, :select_a_language)
  711. end
  712. end
  713. 4 def validate_subject_count
  714. 2082 if subjects.empty?
  715. 100 errors.add(:subjects, :course_creation)
  716. 100 return
  717. end
  718. 1982 case level
  719. when "primary", "further_education"
  720. 1713 if subjects.count > 1
  721. 2 errors.add(:subjects, "has too many subjects")
  722. end
  723. when "secondary"
  724. 262 if subjects.count > 2 && !has_any_modern_language_subject_type?
  725. 1 errors.add(:subjects, "has too many subjects")
  726. end
  727. end
  728. end
  729. 4 def validate_subject_consistency
  730. 2082 subjects_excluding_discontinued = subjects.reject do |subject|
  731. 2033 DiscontinuedSubject.exists?(id: subject.id)
  732. end
  733. 2082 return if subjects_excluding_discontinued.empty?
  734. 1982 case level
  735. when "primary"
  736. 1691 unless PrimarySubject.exists?(id: subjects_excluding_discontinued.map(&:id))
  737. 1 errors.add(:subjects, "must be primary")
  738. end
  739. when "secondary"
  740. 262 unless SecondarySubject.exists?(id: subjects_excluding_discontinued.map(&:id))
  741. 15 errors.add(:subjects, "must be secondary")
  742. end
  743. when "further_education"
  744. 22 unless FurtherEducationSubject.exists?(id: subjects_excluding_discontinued.map(&:id))
  745. 1 errors.add(:subjects, "must be further education")
  746. end
  747. end
  748. end
  749. 4 def validate_custom_age_range
  750. 1837 Courses::ValidateCustomAgeRangeService.new.execute(age_range_in_years, self)
  751. end
  752. 4 def valid_date_range
  753. 183 recruitment_cycle.application_start_date..recruitment_cycle.application_end_date
  754. end
  755. 4 def services
  756. 1552 return @services if @services.present?
  757. 579 @services = Dry::Container.new
  758. 579 @services.register(:generate_course_name) do
  759. 170 Courses::GenerateCourseNameService.new
  760. end
  761. 579 @services.register(:assignable_master_subjects) do
  762. 186 Courses::AssignableMasterSubjectService.new
  763. end
  764. 579 @services.register(:assignable_subjects) do
  765. 1 Courses::AssignableSubjectService.new
  766. end
  767. 579 @services.register(:content_status) do
  768. 1195 Courses::ContentStatusService.new
  769. end
  770. end
  771. 4 def accredited_body_exists_in_current_cycle
  772. 16 return unless accredited_body_code
  773. errors.add(:base, "The Accredited Body #{accredited_body_code} does not exist in this cycle") unless RecruitmentCycle.current.providers.find_by(provider_code: accredited_body_code)
  774. end
  775. end

app/models/course_enrichment.rb

100.0% lines covered

40 relevant lines. 40 lines covered and 0 lines missed.
    
  1. 4 class CourseEnrichment < ApplicationRecord
  2. 4 include TouchCourse
  3. 4 enum status: { draft: 0, published: 1, rolled_over: 2, withdrawn: 3 }
  4. 4 jsonb_accessor :json_data,
  5. about_course: [:string, { store_key: "AboutCourse" }],
  6. course_length: [:string, { store_key: "CourseLength" }],
  7. fee_details: [:string, { store_key: "FeeDetails" }],
  8. fee_international: [:integer, { store_key: "FeeInternational" }],
  9. fee_uk_eu: [:integer, { store_key: "FeeUkEu" }],
  10. financial_support: [:string, { store_key: "FinancialSupport" }],
  11. how_school_placements_work: [:string,
  12. { store_key: "HowSchoolPlacementsWork" }],
  13. interview_process: [:string, { store_key: "InterviewProcess" }],
  14. other_requirements: [:string, { store_key: "OtherRequirements" }],
  15. personal_qualities: [:string, { store_key: "PersonalQualities" }],
  16. required_qualifications: [:string, { store_key: "Qualifications" }],
  17. salary_details: [:string, { store_key: "SalaryDetails" }]
  18. 4 belongs_to :course
  19. 299 scope :most_recent, -> { order(created_at: :desc, id: :desc) }
  20. 13 scope :draft, -> { where(status: "draft").or(rolled_over) }
  21. 4 def draft?
  22. 488 status.in? %w[draft rolled_over]
  23. end
  24. # About this course
  25. # TODO POST MIGRATION: Move out of this as it's handled in the form object
  26. 4 validates :about_course, presence: true, on: :publish
  27. 4 validates :about_course, words_count: { maximum: 400 }
  28. 4 validates :interview_process, words_count: { maximum: 250 }
  29. 4 validates :how_school_placements_work, presence: true, on: :publish
  30. 4 validates :how_school_placements_work, words_count: { maximum: 350 }
  31. # Course length and fees
  32. # TODO: POST MIGRATION: Move out of this as it's handled in the form object
  33. 4 validates :course_length, presence: true, on: :publish
  34. 4 validates :fee_uk_eu, presence: true, on: :publish, if: :is_fee_based?
  35. 4 validates :fee_uk_eu,
  36. numericality: { allow_blank: true,
  37. only_integer: true,
  38. greater_than_or_equal_to: 0,
  39. less_than_or_equal_to: 100000 },
  40. if: :is_fee_based?
  41. 4 validates :fee_international,
  42. numericality: { allow_blank: true,
  43. only_integer: true,
  44. greater_than_or_equal_to: 0,
  45. less_than_or_equal_to: 100000 },
  46. if: :is_fee_based?
  47. 4 validates :fee_details, words_count: { maximum: 250 }, if: :is_fee_based?
  48. 4 validates :financial_support,
  49. words_count: { maximum: 250 },
  50. if: :is_fee_based?
  51. # Course length and salary
  52. # TODO: POST MIGRATION: Move out of this as it's handled in the form object
  53. 4 validates :salary_details, presence: true, on: :publish, unless: :is_fee_based?
  54. 4 validates :salary_details, words_count: { maximum: 250 }, unless: :is_fee_based?
  55. # Requirements and qualifications
  56. 4 validates :required_qualifications, presence: true, on: :publish, if: :required_qualifications_needed?
  57. 4 validates :required_qualifications, words_count: { maximum: 100 }
  58. 4 validates :personal_qualities, words_count: { maximum: 100 }
  59. 4 validates :other_requirements, words_count: { maximum: 100 }
  60. 4 def is_fee_based?
  61. 5473 course&.is_fee_based?
  62. end
  63. 4 def has_been_published_before?
  64. 121 last_published_timestamp_utc.present?
  65. end
  66. 4 def publish(current_user)
  67. 17 update(status: "published",
  68. last_published_timestamp_utc: Time.now.utc,
  69. updated_by_user_id: current_user.id)
  70. end
  71. 4 def unpublish(initial_draft: true)
  72. 4 data = { status: :draft }
  73. 4 data[:last_published_timestamp_utc] = nil if initial_draft
  74. 4 update(data)
  75. end
  76. 4 def withdraw
  77. 6 update(status: "withdrawn")
  78. end
  79. 4 def required_qualifications_needed?
  80. 44 (course&.provider&.recruitment_cycle&.year.to_i) < Course::STRUCTURED_REQUIREMENTS_REQUIRED_FROM
  81. end
  82. end

app/models/course_subject.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 4 class CourseSubject < ApplicationRecord
  2. 4 self.table_name = "course_subject"
  3. 4 belongs_to :course
  4. 4 belongs_to :subject
  5. 4 validates :subject_id, uniqueness: { scope: :course_id }
  6. 4 audited associated_with: :course
  7. end

app/models/financial_incentive.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 4 class FinancialIncentive < ApplicationRecord
  2. 4 belongs_to :subject
  3. end

app/models/interrupt_page_acknowledgement.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 3 class InterruptPageAcknowledgement < ApplicationRecord
  2. 3 belongs_to :user
  3. 3 belongs_to :recruitment_cycle
  4. 3 ALL_PAGES = %w[rollover rollover_recruitment].freeze
  5. 3 enum page: ALL_PAGES.zip(ALL_PAGES).to_h
  6. end

app/models/organisation.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 4 class Organisation < ApplicationRecord
  2. 4 has_many :organisation_users
  3. 4 has_many :users, through: :organisation_users
  4. 4 has_and_belongs_to_many :providers
  5. 4 validates :name, presence: true
  6. 4 has_associated_audits
  7. 4 audited
  8. 4 def add_user(user)
  9. 1 users << user
  10. end
  11. end

app/models/organisation_user.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 4 class OrganisationUser < ApplicationRecord
  2. 4 belongs_to :organisation
  3. 4 belongs_to :user
  4. 4 audited associated_with: :organisation
  5. end

app/models/provider.rb

98.68% lines covered

151 relevant lines. 149 lines covered and 2 lines missed.
    
  1. # Unpleasant hack to stop autoload error on CI
  2. 4 require_relative "../services/providers/generate_course_code_service"
  3. 4 class Provider < ApplicationRecord
  4. 4 include RegionCode
  5. 4 include ChangedAt
  6. 4 include Discard::Model
  7. 4 include PgSearch::Model
  8. 4 CHANGES_INTRODUCED_IN_2022_CYCLE = 2022
  9. 4 before_create :set_defaults
  10. 4 has_associated_audits
  11. 4 audited except: :changed_at
  12. # NOTE: `provider_type` & `accrediting_provider`
  13. # The `unknown` & `invalid_value` provider types can be removed
  14. # Once the underlying the data discreptives has been amended.
  15. # Validation can be added to enforce compliance.
  16. # The `scitt` & `university` provider types can be used to denote
  17. # that they are an `accredited_body` for accrediting providers
  18. # therefore `lead_school` is a `not_an_accredited_body`.
  19. 4 enum provider_type: {
  20. scitt: "B",
  21. lead_school: "Y",
  22. university: "O",
  23. }
  24. 4 enum accrediting_provider: {
  25. accredited_body: "Y",
  26. not_an_accredited_body: "N",
  27. }
  28. 4 belongs_to :recruitment_cycle
  29. 4 has_and_belongs_to_many :organisations, join_table: :organisation_provider
  30. 4 has_many :users_via_organisation, -> { kept }, through: :organisations, source: :users
  31. 4 has_many :user_permissions
  32. 173 has_many :users, -> { kept }, through: :user_permissions
  33. 360 has_many :sites, -> { kept }, inverse_of: :provider
  34. 4 has_many :user_notifications,
  35. foreign_key: :provider_code,
  36. primary_key: :provider_code,
  37. inverse_of: :provider
  38. 1800 has_many :courses, -> { kept }, inverse_of: false
  39. 4 has_one :ucas_preferences, class_name: "ProviderUCASPreference"
  40. 4 has_many :contacts
  41. 4 has_many :accredited_courses, # use current_accredited_courses to filter to courses in the same cycle as this provider
  42. 50 -> { where(discarded_at: nil) },
  43. class_name: "Course",
  44. foreign_key: :accredited_body_code,
  45. primary_key: :provider_code,
  46. inverse_of: :accrediting_provider
  47. # We have a has_many relationship rather than has_one as
  48. # there are lead_schools that have PE courses ratified by more than
  49. # one Accredited Body
  50. 4 has_many :allocations
  51. # the accredited_providers that this provider is a training_provider for
  52. 106 has_many :accrediting_providers, -> { distinct }, through: :courses
  53. 4 delegate :year, to: :recruitment_cycle, prefix: true
  54. 4 def rollable_courses?
  55. 13 courses.any?(&:rollable?)
  56. end
  57. 4 def rollable_accredited_courses?
  58. accredited_courses.any?(&:rollable?)
  59. end
  60. 4 def rollable?
  61. 13 rollable_courses? || rollable_accredited_courses?
  62. end
  63. 4 def rolled_over?
  64. 292 FeatureService.enabled?("rollover.can_edit_current_and_next_cycles")
  65. end
  66. # the providers that this provider is an accredited_provider for
  67. 4 def training_providers
  68. 51 Provider.where(id: current_accredited_courses.pluck(:provider_id))
  69. end
  70. 4 def current_accredited_courses
  71. 52 accredited_courses.includes(:provider).where(provider: { recruitment_cycle: recruitment_cycle })
  72. end
  73. 4 scope :changed_since, lambda { |timestamp|
  74. 5 if timestamp.present?
  75. 5 where("provider.changed_at > ?", timestamp)
  76. else
  77. where("changed_at is not null")
  78. end.order(:changed_at, :id)
  79. }
  80. 48 scope :by_name_ascending, -> { order(provider_name: :asc) }
  81. 10 scope :by_name_descending, -> { order(provider_name: :desc) }
  82. 4 scope :by_provider_name, lambda { |provider_name|
  83. 2 order(
  84. Arel.sql(
  85. "CASE WHEN provider.provider_name = #{connection.quote(provider_name)} THEN '1' END",
  86. ),
  87. )
  88. }
  89. 4 scope :with_findable_courses, lambda {
  90. 11 where(id: Course.findable.select(:provider_id))
  91. .or(where(provider_code: Course.findable.select(:accredited_body_code)))
  92. }
  93. 609 scope :in_current_cycle, -> { where(recruitment_cycle: RecruitmentCycle.current_recruitment_cycle) }
  94. 4 scope :with_allocations_for_current_cycle_year, -> { joins(:allocations).merge(Allocation.current_allocations).order(:provider_name) }
  95. 4 scope :not_geocoded, -> { where(latitude: nil, longitude: nil).or where(region_code: nil) }
  96. 4 serialize :accrediting_provider_enrichments, AccreditingProviderEnrichment::ArraySerializer
  97. 4 validates :train_with_us, words_count: { maximum: 250, message: "^Reduce the word count for training with you" }
  98. 4 validates :train_with_disability, words_count: { maximum: 250, message: "^Reduce the word count for training with disabilities and other needs" }
  99. 4 validates :email, email_address: true, if: :email_changed?
  100. 4 validates :provider_name, length: { maximum: 100 }, on: :update
  101. 4 validates :provider_code, uniqueness: { scope: :recruitment_cycle }
  102. 4 validates :provider_type, presence: true
  103. 4 validates :telephone, phone: { message: "^Enter a valid telephone number" }, if: :telephone_changed?
  104. # TODO: Remove this validation once the 2021 recruitment cycle is over
  105. 4 validates :ukprn, reference_number_format: { allow_blank: true, minimum: 8, maximum: 8, message: "^UKPRN must be 8 numbers" }
  106. 367 validates :ukprn, reference_number_format: { allow_blank: false, minimum: 8, maximum: 8, message: "^UKPRN must be 8 numbers" }, if: -> { recruitment_cycle.after_2021? }, on: :update
  107. # TODO: Remove this validation once the 2021 recruitment cycle is over
  108. 4 validates :urn, reference_number_format: { allow_blank: true, minimum: 5, maximum: 6, message: "^URN must be 5 or 6 numbers" }, if: :lead_school?
  109. 367 validates :urn, reference_number_format: { allow_blank: false, minimum: 5, maximum: 6, message: "^URN must be 5 or 6 numbers" }, if: -> { lead_school? && recruitment_cycle.after_2021? }, on: :update
  110. 4 validates :train_with_us, presence: true, on: :update, if: :train_with_us_changed?
  111. 4 validates :train_with_disability, presence: true, on: :update, if: :train_with_disability_changed?
  112. 4 validate :add_enrichment_errors
  113. 4 acts_as_mappable lat_column_name: :latitude, lng_column_name: :longitude
  114. 4 before_discard do
  115. 4 discard_courses
  116. 4 discard_sites
  117. end
  118. 4 pg_search_scope :provider_search,
  119. against: %i[provider_code provider_name],
  120. using: { tsearch: { prefix: true } }
  121. 4 pg_search_scope :course_search,
  122. associated_against: {
  123. courses: %i[course_code],
  124. }, using: { tsearch: { prefix: true } }
  125. 4 accepts_nested_attributes_for :sites
  126. 4 accepts_nested_attributes_for :organisations
  127. 4 attr_accessor :skip_geocoding
  128. 4 after_commit :geocode_provider, unless: :skip_geocoding
  129. 4 def geocode_provider
  130. 2974 GeocodeJob.perform_later("Provider", id) if needs_geolocation?
  131. end
  132. 4 def needs_geolocation?
  133. 2979 full_address.present? && (
  134. 2979 latitude.nil? || longitude.nil? || address_changed?
  135. )
  136. end
  137. 4 def full_address
  138. 2984 address = [provider_name, address1, address2, address3, address4, postcode]
  139. 2984 return "" if address.all?(&:blank?)
  140. 2983 address.compact.join(", ")
  141. end
  142. 4 def full_address_with_breaks
  143. 36 [address1, address2, address3, address4, postcode].map { |line| ERB::Util.html_escape(line) }.select(&:present?).join("<br> ").html_safe
  144. end
  145. 4 def address_changed?
  146. 14 saved_change_to_provider_name? ||
  147. saved_change_to_address1? ||
  148. saved_change_to_address2? ||
  149. saved_change_to_address3? ||
  150. saved_change_to_address4? ||
  151. saved_change_to_postcode?
  152. end
  153. # Currently Provider#contact_info isn't used but will likely be needed when
  154. # we need to expose the candidate-facing contact info.
  155. #
  156. # When the time comes:
  157. # - rename this method to reflect that it's the candidate-facing contact
  158. # - resurrect the tests which were stripped from models/provider_spec.rb
  159. #
  160. # def contact_info
  161. # self
  162. # .attributes_before_type_cast
  163. # .slice('address1', 'address2', 'address3', 'address4', 'postcode', 'region_code', 'telephone', 'email')
  164. # end
  165. # This is used by the providers index; it is a replacement for `.includes(:courses)`,
  166. # but it only fetches the counts for the associated courses. By not fetching all the
  167. # course objects for 1000+ providers, the db query runs much faster, and the view spends
  168. # less time rendering because there's less data to comb through.
  169. 4 def self.include_courses_counts
  170. 1 joins(
  171. 1 <<~EOSQL,
  172. LEFT OUTER JOIN (
  173. SELECT b.provider_id, COUNT(*) courses_count
  174. FROM course b
  175. WHERE b.discarded_at IS NULL
  176. GROUP BY b.provider_id
  177. ) a ON a.provider_id = provider.id
  178. EOSQL
  179. ).select("provider.*, COALESCE(a.courses_count, 0) AS included_courses_count")
  180. end
  181. 4 def self.include_accredited_courses_counts(provider_code)
  182. 3 joins(
  183. 3 <<~EOSQL,
  184. LEFT OUTER JOIN (
  185. SELECT b.provider_id, COUNT(*) courses_count
  186. FROM course b
  187. WHERE b.discarded_at IS NULL
  188. AND b.accredited_body_code = #{ActiveRecord::Base.connection.quote(provider_code)}
  189. GROUP BY b.provider_id
  190. ) a ON a.provider_id = provider.id
  191. EOSQL
  192. ).select("provider.*, COALESCE(a.courses_count, 0) AS included_accredited_courses_count")
  193. end
  194. 4 def courses_count
  195. 2 has_attribute?("included_courses_count") ? included_courses_count : courses.size
  196. end
  197. 4 def accredited_courses_count
  198. 3 has_attribute?("included_accredited_courses_count") ? included_accredited_courses_count : 0
  199. end
  200. 4 def update_changed_at(timestamp: Time.now.utc)
  201. # Changed_at represents changes to related records as well as provider
  202. # itself, so we don't want to alter the semantics of updated_at which
  203. # represents changes to just the provider record.
  204. 3666 update_columns changed_at: timestamp
  205. end
  206. # This reflects the fact that organisations should actually be a has_one.
  207. 4 def organisation
  208. 3 organisations.first
  209. end
  210. 4 def provider_type=(new_value)
  211. 4355 super
  212. 4355 self.accrediting_provider = if scitt? || university?
  213. 474 :accredited_body
  214. else
  215. 3881 :not_an_accredited_body
  216. end
  217. end
  218. 4 def to_s
  219. 1 "#{provider_name} (#{provider_code}) [#{recruitment_cycle}]"
  220. end
  221. 4 def accredited_bodies
  222. 40 accrediting_providers.map do |ap|
  223. 35 accrediting_provider_enrichment = accrediting_provider_enrichment(ap.provider_code)
  224. {
  225. 35 provider_name: ap.provider_name,
  226. provider_code: ap.provider_code,
  227. description: accrediting_provider_enrichment&.Description || "",
  228. }
  229. end
  230. end
  231. 4 def next_available_course_code
  232. 6 services[:generate_unique_course_code].execute(
  233. existing_codes: courses.order(:course_code).pluck(:course_code),
  234. )
  235. end
  236. 4 def discard_courses
  237. 5 courses.each(&:discard)
  238. end
  239. 4 def discard_sites
  240. 5 sites.each(&:discard)
  241. end
  242. 4 def declared_visa_sponsorship?
  243. 22 !can_sponsor_student_visa.nil? && !can_sponsor_skilled_worker_visa.nil?
  244. end
  245. 4 def can_sponsor_all_visas?
  246. 8 can_sponsor_student_visa && can_sponsor_skilled_worker_visa
  247. end
  248. 4 def can_only_sponsor_student_visa?
  249. 7 can_sponsor_student_visa && !can_sponsor_skilled_worker_visa
  250. end
  251. 4 def can_only_sponsor_skilled_worker_visa?
  252. 3 !can_sponsor_student_visa && can_sponsor_skilled_worker_visa
  253. end
  254. 4 def cannot_sponsor_visas?
  255. 2 can_sponsor_student_visa == false && can_sponsor_skilled_worker_visa == false
  256. end
  257. 4 def from_next_recruitment_cycle
  258. 1 Provider.joins(:recruitment_cycle).where(recruitment_cycle: { year: Settings.current_recruitment_cycle_year.succ.to_s }).find_by(provider_code: provider_code)
  259. end
  260. 4 private
  261. 4 scope :course_code_search, ->(course_code) { joins(:courses).merge(Course.case_insensitive_search(course_code)) }
  262. 4 def accrediting_provider_enrichment(provider_code)
  263. 35 accrediting_provider_enrichments&.find do |enrichment|
  264. 18 enrichment.UcasProviderCode == provider_code
  265. end
  266. end
  267. 4 def add_enrichment_errors
  268. 3381 accrediting_provider_enrichments&.each do |item|
  269. 179 accrediting_provider = accrediting_providers.find { |ap| ap.provider_code == item.UcasProviderCode }
  270. 65 if accrediting_provider.present? && item.invalid?
  271. 10 message = "^Reduce the word count for #{accrediting_provider.provider_name}"
  272. 10 errors.add :accredited_bodies, message
  273. end
  274. end
  275. end
  276. 4 def set_defaults
  277. 2943 self.year_code ||= recruitment_cycle.year
  278. end
  279. 4 def services
  280. 6 return @services if @services.present?
  281. 6 @services = Dry::Container.new
  282. 6 @services.register(:generate_unique_course_code) do
  283. 6 Providers::GenerateUniqueCourseCodeService.new(
  284. generate_course_code_service: Providers::GenerateCourseCodeService.new,
  285. )
  286. end
  287. end
  288. end

app/models/provider_ucas_preference.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 4 class ProviderUCASPreference < ApplicationRecord
  2. 4 belongs_to :provider
  3. 4 enum type_of_gt12: {
  4. coming_or_not: "Coming or Not",
  5. coming_enrol: "Coming / Enrol",
  6. not_coming: "Not coming",
  7. no_response: "No response",
  8. },
  9. _prefix: "type_of_gt12"
  10. 4 enum send_application_alerts: {
  11. all: "Yes, required",
  12. none: "No, not required",
  13. my_programmes: "Yes - only my programmes",
  14. accredited_programmes: "Yes - for accredited programmes only",
  15. },
  16. _prefix: "send_application_alerts_for"
  17. 4 def gt12_contact=(gt12_contact)
  18. 1 update(gt12_response_destination: gt12_contact)
  19. end
  20. 4 def application_alert_contact=(application_alert_contact)
  21. 2 update(application_alert_email: application_alert_contact)
  22. end
  23. end

app/models/recruitment_cycle.rb

78.95% lines covered

38 relevant lines. 30 lines covered and 8 lines missed.
    
  1. 4 class RecruitmentCycle < ApplicationRecord
  2. 2348 has_many :providers, -> { kept }, inverse_of: false
  3. # Because this is through a has_many, these associations can't be updated,
  4. # which is a good thing since we don't have a good way to "move" a course or
  5. # a site to a new recruitment_cycle
  6. 4 has_many :courses, through: :providers
  7. 4 has_many :sites, through: :providers
  8. 4 has_many :allocations
  9. 4 validates :year, presence: true
  10. 4 class << self
  11. 4 def current_recruitment_cycle
  12. 2761 find_by(year: Settings.current_recruitment_cycle_year)
  13. end
  14. 4 alias_method :current, :current_recruitment_cycle
  15. 4 def next_recruitment_cycle
  16. 867 current_recruitment_cycle.next
  17. end
  18. 4 alias_method :next, :next_recruitment_cycle
  19. end
  20. 4 def previous
  21. 50 RecruitmentCycle.find_by(year: year.to_i - 1)
  22. end
  23. 4 def next
  24. 926 RecruitmentCycle.find_by(year: year.to_i + 1)
  25. end
  26. 4 def next?
  27. 823 RecruitmentCycle.next_recruitment_cycle == self
  28. end
  29. 4 def current?
  30. 298 RecruitmentCycle.current_recruitment_cycle == self
  31. end
  32. 4 def current_and_open?
  33. 106 current? && FeatureService.enabled?("rollover.has_current_cycle_started?")
  34. end
  35. 4 def to_s
  36. 7 following_year = Date.new(year.to_i, 1, 1) + 1.year
  37. 7 "#{year}/#{following_year.strftime('%y')}"
  38. end
  39. 4 def title
  40. if current_and_open?
  41. "Current cycle (#{year_range})"
  42. elsif current?
  43. "New cycle (#{year_range})"
  44. elsif next?
  45. "Next cycle (#{year_range})"
  46. else
  47. year_range
  48. end
  49. end
  50. 4 def year_range
  51. "#{year} to #{year.to_i + 1}"
  52. end
  53. # TODO: remove once the 2022 rollover is complete
  54. 4 def after_2021?
  55. 6548 year.to_i >= 2022
  56. end
  57. end

app/models/required_qualifications_summary.rb

97.67% lines covered

43 relevant lines. 42 lines covered and 1 lines missed.
    
  1. 3 class RequiredQualificationsSummary
  2. 3 attr_reader :course
  3. 3 def initialize(course)
  4. 120 @course = course
  5. end
  6. 3 def extract
  7. 120 legacy_qualifications_attribute = course.latest_published_enrichment&.required_qualifications
  8. 120 return legacy_qualifications_attribute if legacy_qualifications_attribute.present?
  9. 59 generate_summary_text
  10. end
  11. 3 private
  12. 3 def generate_summary_text
  13. 59 output = ""
  14. 59 output << required_gcse_content << "\n"
  15. 59 output << pending_gcse_content << "\n"
  16. 59 output << gcse_equivalency_content << "\n"
  17. 59 output << (course.additional_gcse_equivalencies || "") << "\n"
  18. 59 output << degree_grade_content << "\n"
  19. 59 output << degree_subject_requirements_content
  20. 59 output.strip
  21. end
  22. 3 def required_gcse_content
  23. 59 case course.level
  24. when "primary"
  25. 57 "Grade #{course.gcse_grade_required} (C) or above in English, maths and science, or equivalent qualification."
  26. when "secondary"
  27. 1 "Grade #{course.gcse_grade_required} (C) or above in English and maths, or equivalent qualification."
  28. else
  29. 1 ""
  30. end
  31. end
  32. 3 def pending_gcse_content
  33. 59 if course.accept_pending_gcse?
  34. 1 "We will consider candidates with pending GCSEs."
  35. else
  36. 58 "We will not consider candidates with pending GCSEs."
  37. end
  38. end
  39. 3 def gcse_equivalency_content
  40. 59 return "We do not accept equivalency tests." unless course.accept_gcse_equivalency?
  41. 4 case gcse_equivalencies.count
  42. when 0
  43. 1 "" # Assume that course.additional_gcse_equivalencies is populated instead
  44. when 1
  45. 1 "We will accept equivalency tests in #{gcse_equivalencies[0].capitalize}."
  46. when 2
  47. 1 "We will accept equivalency tests in #{gcse_equivalencies[0].capitalize} and #{gcse_equivalencies[1]}."
  48. when 3
  49. 1 "We will accept equivalency tests in #{gcse_equivalencies[0].capitalize}, #{gcse_equivalencies[1]} and #{gcse_equivalencies[2]}."
  50. end
  51. end
  52. 3 def gcse_equivalencies
  53. {
  54. 10 english: course.accept_english_gcse_equivalency?,
  55. maths: course.accept_maths_gcse_equivalency?,
  56. science: course.accept_science_gcse_equivalency?,
  57. 30 }.select { |_k, v| v }.keys
  58. end
  59. 3 def degree_grade_content
  60. {
  61. 59 "two_one" => "An undergraduate degree at class 2:1 or above, or equivalent.",
  62. "two_two" => "An undergraduate degree at class 2:2 or above, or equivalent.",
  63. "third_class" => "An undergraduate degree, or equivalent. This should be an honours degree (Third or above), or equivalent.",
  64. "not_required" => "An undergraduate degree, or equivalent.",
  65. nil => "",
  66. }[course.degree_grade]
  67. end
  68. 3 def degree_subject_requirements_content
  69. 59 if course.additional_degree_subject_requirements?
  70. 59 course.degree_subject_requirements
  71. else
  72. ""
  73. end
  74. end
  75. end

app/models/site.rb

100.0% lines covered

43 relevant lines. 43 lines covered and 0 lines missed.
    
  1. 4 class Site < ApplicationRecord
  2. 4 MAIN_SITE = "main site".freeze
  3. 4 URN_2022_REQUIREMENTS_REQUIRED_FROM = 2022
  4. 4 include PostcodeNormalize
  5. 4 include RegionCode
  6. 4 include TouchProvider
  7. 4 include Discard::Model
  8. 4 POSSIBLE_CODES = (("A".."Z").to_a + ("0".."9").to_a + ["-"]).freeze
  9. 4 EASILY_CONFUSED_CODES = %w[1 I 0 O -].freeze # these ought to be assigned last
  10. 4 DESIRABLE_CODES = (POSSIBLE_CODES - EASILY_CONFUSED_CODES).freeze
  11. 4 before_validation :assign_code, unless: :persisted?
  12. 4 audited associated_with: :provider
  13. 4 has_many :site_statuses, dependent: :destroy
  14. 4 belongs_to :provider
  15. 4 validates :location_name, uniqueness: { scope: :provider_id }
  16. 4 validates :location_name,
  17. :address1,
  18. :postcode,
  19. presence: true
  20. 4 validates :postcode, postcode: true
  21. 835 validates :code, uniqueness: { scope: :provider_id, case_sensitive: false, conditions: -> { where(discarded_at: nil) } },
  22. format: { with: /\A[A-Z0-9\-]+\z/, message: "must contain only A-Z, 0-9 or -" },
  23. presence: true
  24. 4 validates :urn, reference_number_format: { allow_blank: true, minimum: 5, maximum: 6, message: "^URN must be 5 or 6 numbers" }
  25. 4 acts_as_mappable lat_column_name: :latitude, lng_column_name: :longitude
  26. 4 scope :not_geocoded, -> { where(latitude: nil, longitude: nil) }
  27. 4 attr_accessor :skip_geocoding
  28. 4 after_commit :geocode_site, unless: :skip_geocoding
  29. 4 delegate :recruitment_cycle, :provider_code, to: :provider, allow_nil: true
  30. 4 delegate :after_2021?, to: :recruitment_cycle, allow_nil: true, prefix: :recruitment_cycle
  31. 4 def geocode_site
  32. 730 GeocodeJob.perform_later("Site", id) if needs_geolocation?
  33. end
  34. 4 def needs_geolocation?
  35. 735 full_address.present? && (
  36. 734 latitude.nil? || longitude.nil? || address_changed?
  37. )
  38. end
  39. 4 def full_address
  40. 773 address = [address1, address2, address3, address4, postcode]
  41. 773 unless location_name.downcase == MAIN_SITE
  42. 772 address.unshift(location_name)
  43. end
  44. 773 return "" if address.all?(&:blank?)
  45. 770 address.select(&:present?).join(", ")
  46. end
  47. 4 def address_changed?
  48. 60 saved_change_to_location_name? ||
  49. saved_change_to_address1? ||
  50. saved_change_to_address2? ||
  51. saved_change_to_address3? ||
  52. saved_change_to_address4? ||
  53. saved_change_to_postcode?
  54. end
  55. 4 delegate :recruitment_cycle, to: :provider
  56. 4 def assign_code
  57. 701 self.code ||= Sites::CodeGenerator.call(provider: provider)
  58. end
  59. 4 def to_s
  60. 1 "#{location_name} (code: #{code})"
  61. end
  62. end

app/models/site_status.rb

98.33% lines covered

60 relevant lines. 59 lines covered and 1 lines missed.
    
  1. 4 class SiteStatus < ApplicationRecord
  2. 4 include TouchCourse
  3. 4 include AASM
  4. 4 self.table_name = "course_site"
  5. 4 after_initialize :set_defaults
  6. 4 before_validation :set_vac_status
  7. 4 audited associated_with: :course
  8. 4 validate :vac_status_must_be_consistent_with_course_study_mode,
  9. 715 if: proc { |s| s.course&.study_mode.present? }
  10. 4 enum vac_status: {
  11. both_full_time_and_part_time_vacancies: "B",
  12. part_time_vacancies: "P",
  13. full_time_vacancies: "F",
  14. no_vacancies: "",
  15. }
  16. 4 enum status: {
  17. discontinued: "D",
  18. running: "R",
  19. new_status: "N",
  20. suspended: "S",
  21. }, _prefix: :status
  22. 4 enum publish: {
  23. published: "Y",
  24. unpublished: "N",
  25. }, _suffix: :on_ucas
  26. 4 aasm column: :status, enum: true do
  27. 4 state :new_status, initial: true
  28. 4 state :running
  29. 4 state :suspended
  30. 4 state :discontinued
  31. 4 after_all_transitions :update_publish_flag
  32. 4 event :start do
  33. 4 transitions from: %i[new_status suspended discontinued], to: :running
  34. end
  35. 4 event :suspend do
  36. 4 transitions from: :running, to: :suspended
  37. end
  38. end
  39. 4 def update_publish_flag
  40. 37 self.publish = (aasm.to_state == :running ? :published : :unpublished)
  41. end
  42. 4 belongs_to :site
  43. 4 belongs_to :course
  44. 4 acts_as_mappable through: :site
  45. 1072 scope :findable, -> { status_running.published_on_ucas }
  46. 4 def findable?
  47. 579 status_running? && published_on_ucas?
  48. end
  49. 217 scope :with_vacancies, -> { where.not(vac_status: :no_vacancies).findable }
  50. 7 scope :new_or_running, -> { where(status: %i[running new_status]) }
  51. 4 def with_vacancies?
  52. 105 !no_vacancies? && findable?
  53. end
  54. 4 def self.default_vac_status_given(study_mode:)
  55. 102 case study_mode
  56. when "full_time"
  57. 60 :full_time_vacancies
  58. when "part_time"
  59. 35 :part_time_vacancies
  60. when "full_time_or_part_time"
  61. 6 :both_full_time_and_part_time_vacancies
  62. else
  63. 1 :no_vacancies
  64. end
  65. end
  66. 4 def has_vacancies?
  67. 20 with_vacancies?
  68. end
  69. 4 def vacancies_filled?
  70. 2 will_save_change_to_attribute?(:vac_status, to: "no_vacancies") && running?
  71. end
  72. 4 private
  73. 4 def set_defaults
  74. 874 self.status ||= :new_status
  75. 874 self.publish ||= :unpublished
  76. end
  77. 4 def set_vac_status
  78. 715 self.vac_status ||= self.class.default_vac_status_given(study_mode: course.study_mode)
  79. end
  80. 4 def vac_status_must_be_consistent_with_course_study_mode
  81. 714 unless vac_status_consistent_with_course_study_mode?
  82. 8 errors.add(:vac_status, "(#{vac_status}) must be consistent with course study mode #{course.study_mode}")
  83. end
  84. end
  85. 4 def vac_status_consistent_with_course_study_mode?
  86. 714 case vac_status
  87. when "no_vacancies"
  88. 72 true
  89. when "full_time_vacancies"
  90. 568 course.full_time? || course.full_time_or_part_time?
  91. when "part_time_vacancies"
  92. 45 course.part_time? || course.full_time_or_part_time?
  93. when "both_full_time_and_part_time_vacancies"
  94. 29 course.full_time_or_part_time?
  95. else
  96. false
  97. end
  98. end
  99. end

app/models/statistic.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. 1 class Statistic < ApplicationRecord
  2. end

app/models/subject.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 4 class Subject < ApplicationRecord
  2. 4 has_many :course_subjects
  3. 4 has_many :courses, through: :course_subjects
  4. 4 belongs_to :subject_area, foreign_key: :type, inverse_of: :subjects
  5. 4 has_one :financial_incentive
  6. 4 scope :with_subject_codes, lambda { |subject_codes|
  7. 22 where(subject_code: subject_codes)
  8. }
  9. 38 scope :active, -> { where.not(type: "DiscontinuedSubject") }
  10. 4 def secondary_subject?
  11. 2720 type == "SecondarySubject"
  12. end
  13. 4 def to_sym
  14. 1 subject_name.parameterize.underscore.to_sym
  15. end
  16. 4 def to_s
  17. 185 subject_name
  18. end
  19. end

app/models/subject_area.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 4 class SubjectArea < ApplicationRecord
  2. 4 has_many :subjects, foreign_key: :type, inverse_of: :subject_area
  3. 4 self.primary_key = :typename
  4. 13 scope :active, -> { where.not(typename: "DiscontinuedSubject") }
  5. end

app/models/subjects/discontinued_subject.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. 4 class DiscontinuedSubject < Subject
  2. end

app/models/subjects/further_education_subject.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 4 class FurtherEducationSubject < Subject
  2. 4 def self.instance
  3. 7 find_by(subject_name: "Further education")
  4. end
  5. end

app/models/subjects/modern_languages_subject.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. 4 class ModernLanguagesSubject < Subject
  2. end

app/models/subjects/primary_subject.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. 4 class PrimarySubject < Subject
  2. end

app/models/subjects/secondary_subject.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 4 class SecondarySubject < Subject
  2. 4 def self.modern_languages
  3. 4503 @modern_languages ||= find_by(subject_name: "Modern Languages")
  4. end
  5. 4 def self.clear_modern_languages_cache
  6. 24 @modern_languages = nil
  7. end
  8. end

app/models/user.rb

100.0% lines covered

53 relevant lines. 53 lines covered and 0 lines missed.
    
  1. 4 class User < ApplicationRecord
  2. 4 include Discard::Model
  3. 4 include PgSearch::Model
  4. 4 has_many :organisation_users
  5. # dependent destroy because https://stackoverflow.com/questions/34073757/removing-relations-is-not-being-audited-by-audited-gem/34078860#34078860
  6. 4 has_many :organisations, through: :organisation_users, dependent: :destroy
  7. 4 has_many :user_notifications, class_name: "UserNotification"
  8. 4 has_many :providers_via_organisations, through: :organisations, source: :providers
  9. 4 has_many :user_permissions
  10. 4 has_many :providers, through: :user_permissions
  11. 4 has_many :access_requests,
  12. foreign_key: :requester_id,
  13. primary_key: :id,
  14. inverse_of: "requester"
  15. 4 has_many :interrupt_page_acknowledgements
  16. 6 scope :admins, -> { where(admin: true) }
  17. 8 scope :non_admins, -> { where.not(admin: true) }
  18. 15 scope :active, -> { where.not(accept_terms_date_utc: nil) }
  19. 4 scope :last_login_since, lambda { |timestamp|
  20. 11 where("last_login_date_utc > ?", timestamp)
  21. }
  22. 4 scope :course_update_subscribers, lambda { |accredited_body_code|
  23. 14 joins(:user_notifications).merge(UserNotification.course_update_notification_requests(accredited_body_code))
  24. }
  25. 4 scope :course_publish_subscribers, lambda { |accredited_body_code|
  26. 22 joins(:user_notifications).merge(UserNotification.course_publish_notification_requests(accredited_body_code))
  27. }
  28. 4 pg_search_scope :search, against: %i[first_name last_name email], using: { tsearch: { prefix: true } }
  29. 4 validates :first_name, presence: true
  30. 4 validates :last_name, presence: true
  31. 4 validates :email, presence: true, format: { with: /\A.*@.*\z/, message: "must contain @" }, uniqueness: true
  32. 4 validate :email_is_lowercase
  33. 4 validates :email, if: :admin?, format: {
  34. with: /\A.*@(digital\.){0,1}education\.gov\.uk\z/,
  35. message: "must be an @[digital.]education.gov.uk domain",
  36. }
  37. 4 audited
  38. 4 def to_s
  39. 1 "#{first_name} #{last_name} <#{email}>"
  40. end
  41. 4 def remove_access_to(providers_to_remove)
  42. 10 self.providers = providers - Array(providers_to_remove)
  43. end
  44. 4 def associated_with_accredited_body?
  45. 559 providers
  46. .in_current_cycle
  47. .accredited_body
  48. .count
  49. .positive?
  50. end
  51. 4 def notifications_configured?
  52. 2 user_notifications.count.positive?
  53. end
  54. 4 def full_name
  55. 42 "#{first_name} #{last_name}"
  56. end
  57. 4 def accepted_terms?
  58. 1065 accept_terms_date_utc.present?
  59. end
  60. 4 def current_rollover_acceptance
  61. 7 current_page_acknowledgement_for("rollover")
  62. end
  63. 4 def current_rollover_recruitment_acceptance
  64. 5 current_page_acknowledgement_for("rollover_recruitment")
  65. end
  66. 4 def has_multiple_providers?
  67. 259 providers.count > 1
  68. end
  69. 4 def has_multiple_providers_in_current_recruitment_cycle?
  70. 17 providers_via_organisations.where(recruitment_cycle: RecruitmentCycle.current).count > 1
  71. end
  72. 4 private
  73. 4 def email_is_lowercase
  74. 1381 if email.present? && email.downcase != email
  75. 3 errors.add(:email, "must be lowercase")
  76. end
  77. end
  78. 4 def current_page_acknowledgement_for(page)
  79. 12 interrupt_page_acknowledgements
  80. .includes(:recruitment_cycle).find_by(page: page, recruitment_cycle: { year: Settings.current_recruitment_cycle_year })
  81. end
  82. end

app/models/user_notification.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 4 class UserNotification < ApplicationRecord
  2. 4 belongs_to :user,
  3. inverse_of: :user_notifications
  4. 4 belongs_to :provider,
  5. foreign_key: :provider_code,
  6. primary_key: :provider_code,
  7. inverse_of: :user_notifications
  8. 4 validates :course_publish, :course_update, inclusion: { in: [true, false] }
  9. 4 scope :course_publish_notification_requests, lambda { |provider_code|
  10. 23 where(provider_code: provider_code, course_publish: true)
  11. }
  12. 4 scope :course_update_notification_requests, lambda { |provider_code|
  13. 19 where(provider_code: provider_code, course_update: true)
  14. }
  15. end

app/models/user_notification_preferences.rb

100.0% lines covered

32 relevant lines. 32 lines covered and 0 lines missed.
    
  1. 3 class UserNotificationPreferences
  2. 3 def initialize(user_id:)
  3. 33 @user_id = user_id
  4. end
  5. 3 def id
  6. 93 @user_id&.to_i
  7. end
  8. 3 def enabled
  9. # course_publish and course_update currently always have the same value
  10. 16 user_notifications.select(&:course_publish).any?
  11. end
  12. 3 def updated_at
  13. 20 return if user_notifications.empty?
  14. 9 user_notifications.maximum(:updated_at).iso8601
  15. end
  16. 3 def update(enable_notifications:)
  17. 18 UserNotification.transaction do
  18. 18 rollback_on_error do
  19. 18 UserNotification.where(user_id: id).destroy_all
  20. 18 user_accredited_body_codes.each do |provider_code|
  21. 29 UserNotification.create(
  22. user_id: id,
  23. course_publish: enable_notifications,
  24. course_update: enable_notifications,
  25. provider_code: provider_code,
  26. )
  27. end
  28. end
  29. end
  30. 18 reset_user_notifications
  31. 18 self
  32. end
  33. 3 alias_method :enabled?, :enabled
  34. 3 private
  35. 3 def user_accredited_body_codes
  36. 18 user.providers.accredited_body.in_current_cycle.distinct.pluck(:provider_code)
  37. end
  38. 3 def user_notifications
  39. 45 @user_notifications ||= UserNotification.where(user_id: id)
  40. end
  41. 3 def reset_user_notifications
  42. 18 @user_notifications = nil
  43. end
  44. 3 def user
  45. 18 @user ||= User.find(id)
  46. end
  47. 3 def rollback_on_error
  48. 18 yield
  49. rescue StandardError => e
  50. 2 Sentry.capture_exception(e)
  51. 2 raise ActiveRecord::Rollback
  52. end
  53. end

app/models/user_permission.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 4 class UserPermission < ApplicationRecord
  2. 4 belongs_to :user
  3. 4 belongs_to :provider
  4. end

app/models/user_session.rb

96.55% lines covered

29 relevant lines. 28 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 class UserSession
  3. 4 attr_reader :email, :sign_in_user_id
  4. 4 attr_accessor :first_name, :last_name
  5. 4 def initialize(email:, sign_in_user_id:, first_name:, last_name:, id_token: nil, provider: "dfe")
  6. 1262 @email = email&.downcase
  7. 1262 @sign_in_user_id = sign_in_user_id
  8. 1262 @first_name = first_name
  9. 1262 @last_name = last_name
  10. 1262 @id_token = id_token
  11. 1262 @provider = provider&.to_s
  12. end
  13. 4 def self.begin_session!(session, omniauth_payload)
  14. 225 session["user"] = {
  15. "email" => omniauth_payload["info"]["email"],
  16. "sign_in_user_id" => omniauth_payload["uid"],
  17. "first_name" => omniauth_payload["info"]["first_name"],
  18. "last_name" => omniauth_payload["info"]["last_name"],
  19. "last_active_at" => Time.zone.now,
  20. "id_token" => omniauth_payload["credentials"]["id_token"],
  21. "provider" => omniauth_payload["provider"],
  22. }
  23. end
  24. 4 def self.load_from_session(session)
  25. 2077 user_session = session["user"]
  26. 2077 return unless user_session
  27. 1259 return if user_session.fetch("last_active_at") < 2.hours.ago
  28. 1258 user_session["last_active_at"] = Time.zone.now
  29. 1258 new(
  30. email: user_session["email"],
  31. sign_in_user_id: user_session["sign_in_user_id"],
  32. first_name: user_session["first_name"],
  33. last_name: user_session["last_name"],
  34. id_token: user_session["id_token"],
  35. provider: user_session["provider"],
  36. )
  37. end
  38. 4 def self.end_session!(session)
  39. 3 session.delete("user")
  40. end
  41. 4 def logout_url
  42. 1 if AuthenticationService.magic_link? || AuthenticationService.persona?
  43. "/sign-in"
  44. else
  45. 1 dfe_logout_url
  46. end
  47. end
  48. 4 private
  49. 4 def dfe_logout_url
  50. 1 uri = URI("#{Settings.dfe_signin.issuer}/session/end")
  51. 1 uri.query = {
  52. id_token_hint: @id_token,
  53. post_logout_redirect_uri: "#{Settings.base_url}/auth/dfe/signout",
  54. }.to_query
  55. 1 uri.to_s
  56. end
  57. end

app/policies/access_request_policy.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 2 class AccessRequestPolicy
  2. 2 attr_reader :user, :access_request
  3. 2 def initialize(user, _access_request)
  4. 17 @user = user
  5. end
  6. 2 def approve?
  7. 15 @user.admin?
  8. end
  9. 2 def create?
  10. 2 @user.present?
  11. end
  12. 2 alias_method :new?, :create?
  13. 2 alias_method :index?, :approve?
  14. 2 alias_method :show?, :approve?
  15. 2 alias_method :inform_publisher?, :approve?
  16. 2 alias_method :confirm?, :approve?
  17. 2 alias_method :destroy?, :approve?
  18. end

app/policies/allocation_policy.rb

97.06% lines covered

34 relevant lines. 33 lines covered and 1 lines missed.
    
  1. 3 class AllocationPolicy
  2. 3 attr_reader :user, :allocation
  3. 3 class Scope
  4. 3 attr_reader :user, :scope
  5. 3 def initialize(user, scope)
  6. 3 @user = user
  7. 3 @scope = scope
  8. end
  9. 3 def resolve
  10. 3 if user.admin?
  11. 1 scope.all
  12. else
  13. 2 scope
  14. .where(accredited_body_id: user.providers.pluck(:id))
  15. end
  16. end
  17. end
  18. 3 def initialize(user, allocation)
  19. 48 @allocation = allocation
  20. 48 @user = user
  21. end
  22. 3 def index?
  23. 2 user.present?
  24. end
  25. 3 def show?
  26. 6 user_is_admin_or_belongs_to_accredited_body?
  27. end
  28. 3 def create?
  29. 7 user_is_admin_or_belongs_to_accredited_body?
  30. end
  31. 3 def update?
  32. 33 user_is_admin_or_belongs_to_accredited_body?
  33. end
  34. 3 def destroy?
  35. user_is_admin_or_belongs_to_accredited_body?
  36. end
  37. 3 alias_method :edit?, :update?
  38. 3 alias_method :delete?, :update?
  39. 3 alias_method :confirm_deletion?, :index?
  40. 3 alias_method :initial_request?, :index?
  41. 3 alias_method :new_repeat_request?, :index?
  42. 3 private
  43. 3 def user_belongs_to_the_accredited_body?
  44. 46 user.providers.include?(allocation.accredited_body)
  45. end
  46. 3 def user_is_admin_or_belongs_to_accredited_body?
  47. 46 user_belongs_to_the_accredited_body? || user.admin?
  48. end
  49. end

app/policies/contact_policy.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 class ContactPolicy
  2. 1 attr_reader :user, :contact
  3. 1 def initialize(user, contact)
  4. 6 @user = user
  5. 6 @contact = contact
  6. end
  7. 1 def show?
  8. 3 user_is_admin_or_belongs_to_provider?
  9. end
  10. 1 def update?
  11. 3 user_is_admin_or_belongs_to_provider?
  12. end
  13. 1 private
  14. 1 def user_belongs_to_the_provider?
  15. 6 user.providers.include?(contact.provider)
  16. end
  17. 1 def user_is_admin_or_belongs_to_provider?
  18. 6 user_belongs_to_the_provider? || user.admin?
  19. end
  20. end

app/policies/course_policy.rb

97.5% lines covered

40 relevant lines. 39 lines covered and 1 lines missed.
    
  1. 4 class CoursePolicy
  2. 4 attr_reader :user, :course
  3. 4 class Scope
  4. 4 attr_reader :user, :scope
  5. 4 def initialize(user, scope)
  6. 4 @user = user
  7. 4 @scope = scope
  8. end
  9. 4 def resolve
  10. 4 if user.admin?
  11. 1 scope.all
  12. else
  13. 3 scope
  14. .where(provider_id: user.providers.pluck(:id))
  15. .or(Course.where(accredited_body_code: user.providers.pluck(:provider_code)))
  16. end
  17. end
  18. end
  19. 4 def initialize(user, course)
  20. 606 @user = user
  21. 606 @course = course
  22. end
  23. 4 def index?
  24. 2 user.present?
  25. end
  26. 4 def show?
  27. 56 user.admin? || user.providers.include?(course.provider)
  28. end
  29. 4 def send_vacancies_updated_notification?
  30. user.present?
  31. end
  32. 4 alias_method :preview?, :show?
  33. 4 alias_method :details?, :show?
  34. 4 alias_method :update?, :show?
  35. 4 alias_method :edit?, :show?
  36. 4 alias_method :destroy?, :show?
  37. 4 alias_method :publish?, :update?
  38. 4 alias_method :publishable?, :update?
  39. 4 alias_method :new?, :index?
  40. 4 alias_method :withdraw?, :show?
  41. 4 def permitted_attributes
  42. 4 if user.admin?
  43. 2 permitted_admin_attributes
  44. else
  45. 2 permitted_user_attributes
  46. end
  47. end
  48. 4 def permitted_new_course_attributes
  49. 550 %i[
  50. accredited_body_code
  51. age_range_in_years
  52. applications_open_from
  53. funding_type
  54. is_send
  55. level
  56. qualification
  57. start_date
  58. study_mode
  59. ]
  60. end
  61. 4 private
  62. 4 def permitted_user_attributes
  63. 4 permitted_new_course_attributes + %i[
  64. english
  65. maths
  66. science
  67. degree_grade
  68. additional_degree_subject_requirements
  69. degree_subject_requirements
  70. accept_pending_gcse
  71. accept_gcse_equivalency
  72. accept_english_gcse_equivalency
  73. accept_maths_gcse_equivalency
  74. accept_science_gcse_equivalency
  75. additional_gcse_equivalencies
  76. ]
  77. end
  78. 4 def permitted_admin_attributes
  79. 2 permitted_user_attributes + [:name]
  80. end
  81. end

app/policies/organisation_policy.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 class OrganisationPolicy
  2. 1 attr_reader :user, :accessed_organisation
  3. 1 def initialize(user, accessed_organisation)
  4. 2 @user = user
  5. 2 @accessed_organisation = accessed_organisation
  6. end
  7. 1 def add_user?
  8. 2 @user.admin?
  9. end
  10. 1 alias_method :index?, :add_user?
  11. end

app/policies/provider_policy.rb

92.16% lines covered

51 relevant lines. 47 lines covered and 4 lines missed.
    
  1. 4 class ProviderPolicy
  2. 4 attr_reader :user, :provider
  3. 4 class Scope
  4. 4 attr_reader :user, :scope
  5. 4 def initialize(user, scope)
  6. 1 @user = user
  7. 1 @scope = scope
  8. end
  9. 4 def resolve
  10. 1 if user.admin?
  11. scope.all
  12. else
  13. 1 scope.where(id: user.providers)
  14. end
  15. end
  16. end
  17. 4 def initialize(user, provider)
  18. 784 @user = user
  19. 784 @provider = provider
  20. end
  21. 4 def index?
  22. 259 user.present?
  23. end
  24. 4 def search?
  25. user.admin?
  26. end
  27. 4 def show?
  28. 514 user.admin? || user.providers.include?(provider)
  29. end
  30. 4 def show_any?
  31. user.present?
  32. end
  33. 4 def create?
  34. 3 user.admin?
  35. end
  36. 4 def suggest?
  37. 1 user.present?
  38. end
  39. 4 def suggest_any?
  40. user.present?
  41. end
  42. 4 def can_show_training_provider?
  43. 4 return true if user.admin?
  44. 4 accredited_bodies_codes = provider.accredited_bodies.map { |ab| ab[:provider_code] }
  45. 2 user_provider_codes = user.providers.pluck(:provider_code)
  46. 2 !(accredited_bodies_codes & user_provider_codes).compact.empty?
  47. end
  48. 4 alias_method :can_list_sites?, :show?
  49. 4 alias_method :can_create_sites?, :show?
  50. 4 alias_method :can_create_course?, :show?
  51. 4 alias_method :edit?, :show?
  52. 4 alias_method :update?, :show?
  53. 4 alias_method :destroy?, :show?
  54. 4 alias_method :build_new?, :show?
  55. 4 alias_method :can_list_training_providers?, :show?
  56. 4 def permitted_provider_attributes
  57. 4 if user.admin?
  58. 2 admin_provider_attributes
  59. else
  60. 2 user_provider_attributes
  61. end
  62. end
  63. 4 private
  64. 4 def user_provider_attributes
  65. 4 base_attributes = %i[
  66. train_with_us
  67. train_with_disability
  68. email
  69. telephone
  70. website
  71. address1
  72. address2
  73. address3
  74. address4
  75. postcode
  76. region_code
  77. ukprn
  78. can_sponsor_skilled_worker_visa
  79. can_sponsor_student_visa
  80. ]
  81. 4 provider.lead_school? ? base_attributes << :urn : base_attributes
  82. end
  83. 4 def admin_provider_attributes
  84. 2 user_provider_attributes + [:provider_name]
  85. end
  86. end

app/policies/recruitment_cycle_policy.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 1 class RecruitmentCyclePolicy
  2. 1 attr_reader :user, :recruitment_cycle
  3. 1 class Scope
  4. 1 attr_reader :user, :scope
  5. 1 def initialize(user, scope)
  6. 1 @user = user
  7. 1 @scope = scope
  8. end
  9. 1 def resolve
  10. # If a user is logged in, they can view all recruitment cycles
  11. 1 scope
  12. end
  13. end
  14. 1 def initialize(user, recycle)
  15. 3 @user = user
  16. 3 @recruitment_cycle = recycle
  17. end
  18. 1 def index?
  19. 1 user.present?
  20. end
  21. 1 def show?
  22. 2 user.present?
  23. end
  24. end

app/policies/site_policy.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 2 class SitePolicy
  2. 2 attr_reader :user, :site
  3. 2 def initialize(user, site)
  4. 4 @user = user
  5. 4 @site = site
  6. end
  7. 2 def index?
  8. 1 user.present?
  9. end
  10. 2 def show?
  11. 3 user.admin? || user.providers.include?(site.provider)
  12. end
  13. 2 alias_method :update?, :show?
  14. 2 alias_method :create?, :show?
  15. end

app/policies/site_status_policy.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 class SiteStatusPolicy
  2. 1 attr_reader :user, :site_status
  3. 1 def initialize(user, site_status)
  4. 2 @user = user
  5. 2 @site_status = site_status
  6. end
  7. 1 def update?
  8. 2 CoursePolicy.new(user, site_status&.course).update?
  9. end
  10. end

app/policies/user_notification_preferences_policy.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 1 class UserNotificationPreferencesPolicy
  2. 1 def initialize(user, user_notification_preferences)
  3. 4 @user = user
  4. 4 @user_notification_preferences = user_notification_preferences
  5. end
  6. 1 def show?
  7. 2 user.present? && user_matches?
  8. end
  9. 1 def update?
  10. 2 user.present? && user_matches?
  11. end
  12. 1 private
  13. 1 attr_reader :user, :user_notification_preferences
  14. 1 def user_matches?
  15. 4 user.id == user_notification_preferences.id
  16. end
  17. end

app/policies/user_policy.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 1 class UserPolicy
  2. 1 attr_reader :user, :accessed_user
  3. 1 def initialize(user, accessed_user)
  4. 9 @user = user
  5. 9 @accessed_user = accessed_user
  6. end
  7. 1 def show?
  8. 6 user == accessed_user
  9. end
  10. 1 def remove_access_to?
  11. 3 user.admin? || user == accessed_user
  12. end
  13. 1 alias_method :update?, :show?
  14. 1 alias_method :accept_transition_screen?, :update?
  15. 1 alias_method :accept_rollover_screen?, :update?
  16. 1 alias_method :accept_terms?, :update?
  17. 1 alias_method :index?, :show?
  18. 1 alias_method :create?, :update?
  19. end

app/serializers/api/public/v1/serializable_course.rb

100.0% lines covered

67 relevant lines. 67 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module Public
  3. 4 module V1
  4. 4 class SerializableCourse < JSONAPI::Serializable::Resource
  5. 4 class << self
  6. 4 def enrichment_attribute(name, enrichment_name = name)
  7. 44 attribute name do
  8. 1111 @object.latest_published_enrichment&.public_send(enrichment_name)
  9. end
  10. end
  11. end
  12. 4 type "courses"
  13. 4 belongs_to :accredited_body do
  14. 105 data { @object.accrediting_provider }
  15. end
  16. 4 belongs_to :provider
  17. 4 belongs_to :recruitment_cycle
  18. 4 attributes :accredited_body_code,
  19. :age_maximum,
  20. :age_minimum,
  21. :bursary_amount,
  22. :bursary_requirements,
  23. :created_at,
  24. :funding_type,
  25. :gcse_subjects_required,
  26. :level,
  27. :name,
  28. :program_type,
  29. :qualifications,
  30. :scholarship_amount,
  31. :study_mode,
  32. :uuid,
  33. :degree_grade,
  34. :degree_subject_requirements,
  35. :accept_pending_gcse,
  36. :accept_gcse_equivalency,
  37. :accept_english_gcse_equivalency,
  38. :accept_maths_gcse_equivalency,
  39. :accept_science_gcse_equivalency,
  40. :additional_gcse_equivalencies
  41. 4 attribute :about_accredited_body do
  42. 101 @object.accrediting_provider_description
  43. end
  44. 4 attribute :applications_open_from do
  45. 101 @object.applications_open_from&.iso8601
  46. end
  47. 4 attribute :changed_at do
  48. 101 @object.changed_at&.iso8601
  49. end
  50. 4 attribute :code do
  51. 101 @object.course_code
  52. end
  53. 4 attribute :created_at do
  54. 101 @object.created_at&.iso8601
  55. end
  56. 4 attribute :findable do
  57. 101 @object.findable?
  58. end
  59. 4 attribute :has_early_career_payments do
  60. 101 @object.has_early_career_payments?
  61. end
  62. 4 attribute :has_scholarship do
  63. 101 @object.has_scholarship?
  64. end
  65. 4 attribute :has_vacancies do
  66. 101 @object.has_vacancies?
  67. end
  68. 4 attribute :is_send do
  69. 101 @object.is_send?
  70. end
  71. 4 attribute :last_published_at do
  72. 101 @object.last_published_at&.iso8601
  73. end
  74. 4 attribute :open_for_applications do
  75. 101 @object.open_for_applications?
  76. end
  77. 4 attribute :required_qualifications_english do
  78. 101 @object.english
  79. end
  80. 4 attribute :required_qualifications_maths do
  81. 101 @object.maths
  82. end
  83. 4 attribute :required_qualifications_science do
  84. 101 @object.science
  85. end
  86. 4 attribute :running do
  87. 101 @object.findable?
  88. end
  89. 4 attribute :start_date do
  90. 101 @object.start_date&.strftime("%B %Y")
  91. end
  92. 4 attribute :state do
  93. 101 @object.content_status
  94. end
  95. 4 attribute :summary do
  96. 101 @object.description
  97. end
  98. 4 attribute :subject_codes do
  99. 101 @object.subjects.pluck(:subject_code).compact
  100. end
  101. 4 attribute :required_qualifications do
  102. 101 @object.required_qualifications
  103. end
  104. 4 enrichment_attribute :about_course
  105. 4 enrichment_attribute :course_length
  106. 4 enrichment_attribute :fee_details
  107. 4 enrichment_attribute :fee_international
  108. 4 enrichment_attribute :fee_domestic, :fee_uk_eu
  109. 4 enrichment_attribute :financial_support
  110. 4 enrichment_attribute :how_school_placements_work
  111. 4 enrichment_attribute :interview_process
  112. 4 enrichment_attribute :other_requirements
  113. 4 enrichment_attribute :personal_qualities
  114. 4 enrichment_attribute :salary_details
  115. end
  116. end
  117. end
  118. end

app/serializers/api/public/v1/serializable_location.rb

100.0% lines covered

25 relevant lines. 25 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module Public
  3. 4 module V1
  4. 4 class SerializableLocation < JSONAPI::Serializable::Resource
  5. 4 extend JSONAPI::Serializable::Resource::ConditionalFields
  6. 4 type "locations"
  7. 33 belongs_to :course, unless: -> { @course.nil? } do
  8. 32 data { @course }
  9. end
  10. 33 belongs_to :location_status, unless: -> { @location_statuses.nil? } do
  11. 30 data do
  12. # NOTE: This is using arrays otherwise it results in incurring N + 1
  13. 2 @location_statuses.find do |location_status|
  14. 3 location_status.site_id == @object.id
  15. end
  16. end
  17. end
  18. 4 belongs_to :provider
  19. 4 belongs_to :recruitment_cycle
  20. 4 attributes :code,
  21. :urn,
  22. :latitude,
  23. :longitude,
  24. :postcode,
  25. :region_code,
  26. :uuid
  27. 4 attribute :name do
  28. 29 @object.location_name
  29. end
  30. 4 attribute :city do
  31. 29 @object.address3
  32. end
  33. 4 attribute :county do
  34. 29 @object.address4
  35. end
  36. 4 attribute :street_address_1 do
  37. 29 @object.address1
  38. end
  39. 4 attribute :street_address_2 do
  40. 29 @object.address2
  41. end
  42. end
  43. end
  44. end
  45. end

app/serializers/api/public/v1/serializable_location_status.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module Public
  3. 4 module V1
  4. 4 class SerializableLocationStatus < JSONAPI::Serializable::Resource
  5. 4 extend JSONAPI::Serializable::Resource::ConditionalFields
  6. 4 type "location_statuses"
  7. 4 attributes :publish,
  8. :status
  9. 4 attribute :vacancy_status do
  10. 2 @object.vac_status
  11. end
  12. 4 attribute :has_vacancies do
  13. 2 @object.has_vacancies?
  14. end
  15. end
  16. end
  17. end
  18. end

app/serializers/api/public/v1/serializable_provider.rb

100.0% lines covered

25 relevant lines. 25 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module Public
  3. 4 module V1
  4. 4 class SerializableProvider < JSONAPI::Serializable::Resource
  5. 4 type "providers"
  6. 4 belongs_to :recruitment_cycle
  7. 4 attributes :ukprn,
  8. :urn,
  9. :postcode,
  10. :provider_type,
  11. :region_code,
  12. :train_with_disability,
  13. :train_with_us,
  14. :website,
  15. :latitude,
  16. :longitude,
  17. :telephone,
  18. :email,
  19. :can_sponsor_skilled_worker_visa,
  20. :can_sponsor_student_visa
  21. 4 attribute :accredited_body do
  22. 63 @object.accredited_body?
  23. end
  24. 4 attribute :changed_at do
  25. 63 @object.changed_at.iso8601
  26. end
  27. 4 attribute :city do
  28. 63 @object.address3
  29. end
  30. 4 attribute :code do
  31. 63 @object.provider_code
  32. end
  33. 4 attribute :county do
  34. 63 @object.address4
  35. end
  36. 4 attribute :created_at do
  37. 63 @object.created_at.iso8601
  38. end
  39. 4 attribute :name do
  40. 64 @object.provider_name
  41. end
  42. 4 attribute :street_address_1 do
  43. 63 @object.address1
  44. end
  45. 4 attribute :street_address_2 do
  46. 63 @object.address2
  47. end
  48. end
  49. end
  50. end
  51. end

app/serializers/api/public/v1/serializable_provider_suggestion.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 2 module API
  2. 2 module Public
  3. 2 module V1
  4. 2 class SerializableProviderSuggestion < JSONAPI::Serializable::Resource
  5. 2 type "provider_suggestions"
  6. 2 attributes :ukprn,
  7. :urn
  8. 2 attribute :code do
  9. 1 @object.provider_code
  10. end
  11. 2 attribute :name do
  12. 1 @object.provider_name
  13. end
  14. end
  15. end
  16. end
  17. end

app/serializers/api/public/v1/serializable_recruitment_cycle.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module Public
  3. 4 module V1
  4. 4 class SerializableRecruitmentCycle < JSONAPI::Serializable::Resource
  5. 4 type "recruitment_cycles"
  6. 4 attributes :application_start_date,
  7. :application_end_date
  8. 4 attribute :year do
  9. 10 @object.year.to_i
  10. end
  11. end
  12. end
  13. end
  14. end

app/serializers/api/public/v1/serializable_subject.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module Public
  3. 4 module V1
  4. 4 class SerializableSubject < JSONAPI::Serializable::Resource
  5. 4 type "subjects"
  6. 4 attribute :name do
  7. 191 @object.subject_name
  8. end
  9. 4 attribute :code do
  10. 145 @object.subject_code
  11. end
  12. 4 attribute :bursary_amount do
  13. 145 @object.financial_incentive&.bursary_amount
  14. end
  15. 4 attribute :early_career_payments do
  16. 145 @object.financial_incentive&.early_career_payments
  17. end
  18. 4 attribute :scholarship do
  19. 145 @object.financial_incentive&.scholarship
  20. end
  21. 4 attribute :subject_knowledge_enhancement_course_available do
  22. 145 @object.financial_incentive&.subject_knowledge_enhancement_course_available
  23. end
  24. end
  25. end
  26. end
  27. end

app/serializers/api/public/v1/serializable_subject_area.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module Public
  3. 4 module V1
  4. 4 class SerializableSubjectArea < JSONAPI::Serializable::Resource
  5. 4 type "subject_areas"
  6. 4 has_many :subjects
  7. 4 attributes :name, :typename
  8. end
  9. end
  10. end
  11. end

app/serializers/api/public/v1/serializer_service.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module Public
  3. 4 module V1
  4. 4 class SerializerService
  5. 4 include ServicePattern
  6. 4 def call
  7. {
  8. 63 Course: API::Public::V1::SerializableCourse,
  9. Provider: API::Public::V1::SerializableProvider,
  10. RecruitmentCycle: API::Public::V1::SerializableRecruitmentCycle,
  11. Site: API::Public::V1::SerializableLocation,
  12. SiteStatus: API::Public::V1::SerializableLocationStatus,
  13. Subject: API::Public::V1::SerializableSubject,
  14. SubjectArea: API::Public::V1::SerializableSubjectArea,
  15. PrimarySubject: API::Public::V1::SerializableSubject,
  16. SecondarySubject: API::Public::V1::SerializableSubject,
  17. ModernLanguagesSubject: API::Public::V1::SerializableSubject,
  18. FurtherEducationSubject: API::Public::V1::SerializableSubject,
  19. }
  20. end
  21. end
  22. end
  23. end
  24. end

app/serializers/api/v3/serializable_course.rb

100.0% lines covered

43 relevant lines. 43 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module V3
  3. 4 class SerializableCourse < JSONAPI::Serializable::Resource
  4. 4 include TimeFormat
  5. 4 include JsonapiCourseCacheKeyHelper
  6. 4 class << self
  7. 4 def enrichment_attribute(name, enrichment_name = name)
  8. 48 attribute name do
  9. 1176 @object.enrichments
  10. .select(&:published?)
  11. 936 .max_by { |e| [e.created_at, e.id] }
  12. &.__send__(enrichment_name)
  13. end
  14. end
  15. end
  16. 4 type "courses"
  17. 4 attributes :findable?, :open_for_applications?, :has_vacancies?,
  18. :course_code, :name, :study_mode, :qualification, :description,
  19. :content_status, :ucas_status, :funding_type,
  20. :level, :is_send?, :english, :maths, :science, :gcse_subjects_required,
  21. :age_range_in_years, :accrediting_provider,
  22. :accredited_body_code, :level, :changed_at, :uuid, :program_type,
  23. :accept_pending_gcse, :accept_gcse_equivalency,
  24. :accept_english_gcse_equivalency, :accept_maths_gcse_equivalency,
  25. :accept_science_gcse_equivalency, :additional_gcse_equivalencies,
  26. :degree_grade, :additional_degree_subject_requirements,
  27. :degree_subject_requirements
  28. 4 attribute :start_date do
  29. 98 written_month_year(@object.start_date) if @object.start_date
  30. end
  31. 4 attribute :applications_open_from do
  32. 98 @object.applications_open_from&.iso8601
  33. end
  34. 4 attribute :last_published_at do
  35. 98 @object.last_published_at&.iso8601
  36. end
  37. 4 attribute :about_accrediting_body do
  38. 98 @object.accrediting_provider_description
  39. end
  40. 4 attribute :provider_code do
  41. 103 @object.provider.provider_code
  42. end
  43. 4 attribute :recruitment_cycle_year do
  44. 98 @object.recruitment_cycle.year
  45. end
  46. 4 attribute :provider_type do
  47. 98 @object.provider.provider_type
  48. end
  49. 4 belongs_to :provider
  50. 4 belongs_to :accrediting_provider
  51. 4 has_many :site_statuses
  52. 4 has_many :sites
  53. 4 has_many :subjects
  54. 4 enrichment_attribute :about_course
  55. 4 enrichment_attribute :course_length
  56. 4 enrichment_attribute :fee_details
  57. 4 enrichment_attribute :fee_international
  58. 4 enrichment_attribute :fee_uk_eu
  59. 4 enrichment_attribute :financial_support
  60. 4 enrichment_attribute :how_school_placements_work
  61. 4 enrichment_attribute :interview_process
  62. 4 enrichment_attribute :other_requirements
  63. 4 enrichment_attribute :personal_qualities
  64. 4 enrichment_attribute :required_qualifications
  65. 4 enrichment_attribute :salary_details
  66. end
  67. end
  68. end

app/serializers/api/v3/serializable_provider.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module V3
  3. 4 class SerializableProvider < JSONAPI::Serializable::Resource
  4. 4 include JsonapiCacheKeyHelper
  5. 4 type "providers"
  6. 4 attributes :provider_code, :provider_name, :provider_type,
  7. :longitude, :address1, :address2, :address3, :address4,
  8. :postcode, :latitude, :longitude, :can_sponsor_student_visa,
  9. :can_sponsor_skilled_worker_visa, :website, :train_with_us,
  10. :train_with_disability, :email, :telephone
  11. 4 attribute :recruitment_cycle_year do
  12. 73 @object.recruitment_cycle.year
  13. end
  14. end
  15. end
  16. end

app/serializers/api/v3/serializable_recruitment_cycle.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module V3
  3. 4 class SerializableRecruitmentCycle < JSONAPI::Serializable::Resource
  4. 4 type "recruitment_cycles"
  5. 4 attributes :year, :application_start_date, :application_end_date
  6. 4 has_many :providers
  7. end
  8. end
  9. end

app/serializers/api/v3/serializable_site.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module V3
  3. 4 class SerializableSite < JSONAPI::Serializable::Resource
  4. 4 include JsonapiCacheKeyHelper
  5. 4 type "sites"
  6. 4 attributes :code, :location_name, :address1, :address2,
  7. :address3, :address4, :postcode, :region_code,
  8. :latitude, :longitude, :urn
  9. 4 attribute :recruitment_cycle_year do
  10. 27 @object.recruitment_cycle.year
  11. end
  12. end
  13. end
  14. end

app/serializers/api/v3/serializable_site_status.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module V3
  3. 4 class SerializableSiteStatus < JSONAPI::Serializable::Resource
  4. 4 include JsonapiCacheKeyHelper
  5. 4 type "site_statuses"
  6. 4 attributes :vac_status, :publish, :status, :has_vacancies?
  7. 4 has_one :site
  8. end
  9. end
  10. end

app/serializers/api/v3/serializable_subject.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module V3
  3. 4 class SerializableSubject < JSONAPI::Serializable::Resource
  4. 4 include JsonapiCacheKeyHelper
  5. 4 type "subjects"
  6. 4 attributes :subject_name, :subject_code
  7. 4 attribute :bursary_amount do
  8. 103 @object.financial_incentive&.bursary_amount
  9. end
  10. 4 attribute :early_career_payments do
  11. 103 @object.financial_incentive&.early_career_payments
  12. end
  13. 4 attribute :scholarship do
  14. 103 @object.financial_incentive&.scholarship
  15. end
  16. 4 attribute :subject_knowledge_enhancement_course_available do
  17. 103 @object.financial_incentive&.subject_knowledge_enhancement_course_available
  18. end
  19. end
  20. end
  21. end

app/serializers/api/v3/serializable_subject_area.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 4 module API
  2. 4 module V3
  3. 4 class SerializableSubjectArea < JSONAPI::Serializable::Resource
  4. 4 type "subject_areas"
  5. 4 has_many :subjects
  6. 4 attributes :name
  7. 4 attributes :typename
  8. end
  9. end
  10. end

app/serializers/jsonapi_cache_key_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 4 module JsonapiCacheKeyHelper
  2. 4 def jsonapi_cache_key(options)
  3. 41 "#{self.class}/#{@object.cache_key_with_version} " + super(options)
  4. end
  5. end

app/serializers/jsonapi_course_cache_key_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 4 module JsonapiCourseCacheKeyHelper
  2. # When a course has sites updated the course's 'changed_at' field is touched
  3. # We need to add the 'changed_at' value to the cache key so that the cache is refreshed
  4. 4 def jsonapi_cache_key(options)
  5. 59 "#{self.class}/#{@object.cache_key_with_version}/#{@object.changed_at} " + super(options)
  6. end
  7. end

app/services/access_request_approval_service.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 1 class AccessRequestApprovalService
  2. 1 def self.call(access_request)
  3. 14 new(access_request).call
  4. end
  5. 1 def initialize(access_request)
  6. 14 @access_request = access_request
  7. end
  8. 1 def call
  9. 14 target_user = User.find_or_create_by!(email: @access_request.email_address.downcase) do |user|
  10. 8 user.first_name = @access_request.first_name
  11. 8 user.last_name = @access_request.last_name
  12. 8 user.invite_date_utc = Time.now.utc
  13. end
  14. 14 providers_missing_on_target_user = @access_request.requester.providers - target_user.providers
  15. 14 target_user.providers << providers_missing_on_target_user
  16. 14 @access_request.approve
  17. end
  18. end

app/services/allocation_copy_data_service.rb

82.35% lines covered

34 relevant lines. 28 lines covered and 6 lines missed.
    
  1. 1 class AllocationCopyDataService
  2. 1 include ServicePattern
  3. 1 def initialize(allocation_cycle_year:, summary: false)
  4. 1 @allocation_cycle_year = allocation_cycle_year
  5. 1 @summary = summary
  6. end
  7. 1 def call
  8. 1 copied = 0
  9. 1 skipped = 0
  10. 1 Allocation.where(recruitment_cycle_id: previous_allocation_cycle.id).find_each do |prev_alloc|
  11. 2 if prev_alloc.confirmed_number_of_places.to_i.zero? ||
  12. allocation_exists?(prev_alloc)
  13. skipped += 1
  14. else
  15. 2 provider = find_provider(prev_alloc)
  16. 2 accredited_body = find_accredited_body(prev_alloc)
  17. 2 create_allocation(prev_alloc, provider, accredited_body)
  18. 2 copied += 1
  19. end
  20. end
  21. 1 if @summary
  22. Rails.logger.info "###################################"
  23. Rails.logger.info "## Allocation copying task complete"
  24. Rails.logger.info { "# copied: #{copied}" }
  25. Rails.logger.info { "# skipped: #{skipped}" }
  26. Rails.logger.info "###################################"
  27. end
  28. end
  29. 1 private
  30. 1 def create_allocation(prev_alloc, provider, accredited_body)
  31. 2 Allocation.create!(
  32. provider_code: prev_alloc.provider_code,
  33. accredited_body_code: prev_alloc.accredited_body_code,
  34. recruitment_cycle_id: allocation_cycle.id,
  35. provider_id: provider.id,
  36. accredited_body_id: accredited_body.id,
  37. number_of_places: prev_alloc.confirmed_number_of_places,
  38. confirmed_number_of_places: nil,
  39. )
  40. end
  41. 1 def find_provider(prev_alloc)
  42. # Fetch new provider with same code for current recruitment cycle
  43. 2 Provider.where(
  44. provider_code: prev_alloc.provider_code,
  45. recruitment_cycle_id: allocation_cycle.id,
  46. ).first!
  47. end
  48. 1 def find_accredited_body(prev_alloc)
  49. # Fetch new accredited_body with same code for current recruitment cycle
  50. 2 Provider.where(
  51. provider_code: prev_alloc.accredited_body_code,
  52. recruitment_cycle_id: allocation_cycle.id,
  53. accrediting_provider: :accredited_body,
  54. ).first!
  55. end
  56. 1 def allocation_exists?(prev_alloc)
  57. 2 Allocation.exists?(provider_code: prev_alloc.provider_code,
  58. accredited_body_code: prev_alloc.accredited_body_code,
  59. recruitment_cycle_id: allocation_cycle.id)
  60. end
  61. 1 def allocation_cycle
  62. 9 @allocation_cycle ||= RecruitmentCycle.where(year: @allocation_cycle_year).first!
  63. end
  64. 1 def previous_allocation_cycle
  65. 1 allocation_cycle.previous
  66. end
  67. end

app/services/allocation_importer_service.rb

95.24% lines covered

21 relevant lines. 20 lines covered and 1 lines missed.
    
  1. 1 require "csv"
  2. 1 class AllocationImporterService
  3. 1 attr_reader :path_to_csv
  4. 1 def initialize(path_to_csv:)
  5. 4 @path_to_csv = path_to_csv
  6. end
  7. 1 def execute
  8. 5 rows.each do |row|
  9. 5 training_provider = Provider.where(recruitment_cycle_id: 1).find_by(provider_code: row["provider_code"])
  10. 5 accredited_body_provider = Provider.where(recruitment_cycle_id: 1).find_by(provider_code: row["accredited_body_code"])
  11. 5 if training_provider.blank?
  12. 2 raise "Training Provider with code: #{row['provider_code']} not found"
  13. end
  14. 3 if accredited_body_provider.blank?
  15. raise "Accredited Body Provider with code: #{row['accredited_body_code']} not found"
  16. end
  17. 3 Rails.logger.info { "Importing training_provider: #{training_provider.provider_code} and accredited_body: #{accredited_body_provider.provider_code}" }
  18. 3 allocation = Allocation.find_or_initialize_by(
  19. provider: training_provider,
  20. accredited_body: accredited_body_provider,
  21. recruitment_cycle_id: 1,
  22. provider_code: training_provider.provider_code,
  23. accredited_body_code: accredited_body_provider.provider_code,
  24. )
  25. 3 allocation.number_of_places = row["number_of_places"]
  26. 3 if allocation.changed?
  27. 2 allocation.save!
  28. end
  29. end
  30. end
  31. 1 private
  32. 1 def rows
  33. 5 @rows ||= CSV.read(path_to_csv, headers: :first_row)
  34. end
  35. end

app/services/allocation_reporting_service.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 2 class AllocationReportingService
  2. 2 def initialize(recruitment_cycle_scope: RecruitmentCycle)
  3. 11 @current = recruitment_cycle_scope
  4. 11 @previous = recruitment_cycle_scope.previous
  5. end
  6. 2 class << self
  7. 2 def call(recruitment_cycle_scope:)
  8. 11 new(recruitment_cycle_scope: recruitment_cycle_scope).call
  9. end
  10. end
  11. 2 def call
  12. {
  13. 11 previous: reporting(recruitment_cycle: @previous),
  14. current: reporting(recruitment_cycle: @current),
  15. }
  16. end
  17. 2 private_class_method :new
  18. 2 private
  19. 2 def reporting(recruitment_cycle: RecruitmentCycle)
  20. 22 requested_allocations = recruitment_cycle.allocations.not_declined
  21. 22 distinct_requested_allocations = requested_allocations.distinct
  22. {
  23. 22 total: {
  24. allocations: requested_allocations.count,
  25. distinct_accredited_bodies: distinct_requested_allocations.select(:accredited_body_id).count,
  26. distinct_providers: distinct_requested_allocations.select(:provider_id).count,
  27. number_of_places: requested_allocations.sum(:number_of_places),
  28. },
  29. }
  30. end
  31. end

app/services/allocations/create.rb

100.0% lines covered

28 relevant lines. 28 lines covered and 0 lines missed.
    
  1. 3 module Allocations
  2. 3 class Create
  3. 3 attr_accessor :object
  4. 3 def initialize(params)
  5. 4 @object = Allocation.new(params)
  6. 4 set_codes
  7. 4 set_recruitment_cycle
  8. 4 set_number_of_places
  9. 4 object
  10. end
  11. 3 def execute
  12. 4 object.save
  13. end
  14. 3 private
  15. 3 def set_number_of_places
  16. 4 other_request_type_number = 0
  17. 4 if object.number_of_places.nil?
  18. 3 object.number_of_places = if object.repeat?
  19. 1 object.previous&.number_of_places || 0
  20. else
  21. 2 other_request_type_number
  22. end
  23. end
  24. end
  25. 3 def set_codes
  26. 4 object.accredited_body_code ||= accredited_body_code
  27. 4 object.provider_code ||= provider_code
  28. end
  29. 3 def accredited_body_code
  30. 4 object.accredited_body.provider_code
  31. end
  32. 3 def provider_code
  33. 4 return unless object.provider
  34. 4 object.provider.provider_code
  35. end
  36. 3 def set_recruitment_cycle
  37. 4 object.recruitment_cycle_id ||= RecruitmentCycle.find_by(year: Allocation::ALLOCATION_CYCLE_YEAR).id
  38. end
  39. end
  40. end

app/services/allocations/update.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 1 module Allocations
  2. 1 class Update
  3. 1 attr_accessor :object, :params
  4. 1 def initialize(allocation, params)
  5. 13 @object = allocation
  6. 13 @params = params
  7. 13 set_number_of_places
  8. 13 set_request_type
  9. 13 object
  10. end
  11. 1 def execute
  12. 13 object.save
  13. end
  14. 1 private
  15. 1 def set_number_of_places
  16. 13 object.number_of_places = case params[:request_type]
  17. when "declined"
  18. 3 0
  19. when "repeat"
  20. 6 object.previous&.number_of_places || 0
  21. else
  22. 4 params[:number_of_places]
  23. end
  24. end
  25. 1 def set_request_type
  26. 13 object.request_type = params[:request_type]
  27. end
  28. end
  29. end

app/services/authentication_service.rb

97.78% lines covered

90 relevant lines. 88 lines covered and 2 lines missed.
    
  1. 4 class AuthenticationService
  2. 4 attr_accessor :encoded_token, :user
  3. 4 def initialize(logger:)
  4. 23 @logger = logger
  5. end
  6. 4 class << self
  7. 4 DFE_SIGNIN = "dfe_signin".freeze
  8. 4 PERSONA = "persona".freeze
  9. 4 MAGIC_LINK = "magic_link".freeze
  10. 4 def mode
  11. 1162 case Settings.authentication.mode
  12. when MAGIC_LINK
  13. MAGIC_LINK
  14. when PERSONA
  15. PERSONA
  16. else
  17. 1162 DFE_SIGNIN
  18. end
  19. end
  20. 4 def dfe_signin?
  21. 223 mode == DFE_SIGNIN
  22. end
  23. 4 def magic_link?
  24. 596 mode == MAGIC_LINK
  25. end
  26. 4 def persona?
  27. 366 mode == PERSONA
  28. end
  29. end
  30. 4 def execute(encoded_token)
  31. 23 @encoded_token = encoded_token
  32. 23 @user = find_user_by_sign_in_user_id || find_user_by_email
  33. 23 update_user_information if user
  34. 23 user
  35. end
  36. 4 private
  37. 4 attr_reader :logger
  38. 4 def decoded_token
  39. 217 @decoded_token ||= Token::DecodeService.call(
  40. encoded_token: encoded_token,
  41. secret: Settings.authentication.secret,
  42. algorithm: Settings.authentication.algorithm,
  43. audience: Settings.authentication.audience,
  44. issuer: Settings.authentication.issuer,
  45. subject: Settings.authentication.subject,
  46. )
  47. end
  48. 4 def email_from_token
  49. 61 decoded_token["email"]&.downcase
  50. end
  51. 4 def sign_in_user_id_from_token
  52. 82 decoded_token["sign_in_user_id"]
  53. end
  54. 4 def first_name_from_token
  55. 37 decoded_token["first_name"]
  56. end
  57. 4 def last_name_from_token
  58. 37 decoded_token["last_name"]
  59. end
  60. 4 def update_user_information
  61. 19 update_user_first_name
  62. 19 update_user_last_name
  63. 19 update_user_sign_in_id
  64. 19 update_user_email
  65. 19 user.save!
  66. end
  67. 4 def find_user_by_email
  68. 16 if email_from_token.blank?
  69. 2 log_message(:debug, user, "No email in token")
  70. 2 return
  71. end
  72. 14 if (user = User.find_by("lower(email) = ?", email_from_token))
  73. 9 log_message(:info, user, "User found by email address")
  74. end
  75. 14 user
  76. end
  77. 4 def find_user_by_sign_in_user_id
  78. 23 if sign_in_user_id_from_token.blank?
  79. 2 log_message(:debug, user, "No sign_in_user_id in token")
  80. 2 return
  81. end
  82. 21 if (user = User.find_by(sign_in_user_id: sign_in_user_id_from_token))
  83. 13 log_message(:info, user, "User found from sign_in_user_id in token", { sign_in_user_id: sign_in_user_id_from_token })
  84. end
  85. 21 user
  86. end
  87. 4 def user_email_does_not_match_token?
  88. 19 user.email&.downcase != email_from_token
  89. end
  90. 4 def user_sign_in_id_does_not_match_token?
  91. 19 return unless user
  92. 19 user.sign_in_user_id != sign_in_user_id_from_token
  93. end
  94. 4 def update_user_email
  95. 19 return unless user_email_does_not_match_token?
  96. 6 if (duplicate_user = find_user_by_email)
  97. # Change: bob@gmail.com => bob_1634828853_gmail.com@example.com
  98. 3 new_email = "#{duplicate_user.email.gsub(/\@/, "_#{Time.now.to_i}_")}@example.com"
  99. 3 duplicate_user.update!(email: new_email)
  100. end
  101. 6 log_message(:debug, user, "Updating user email for", { new_email_md5: "MD5:#{obfuscate_email(email_from_token)}" })
  102. 6 user.email = email_from_token
  103. end
  104. 4 def update_user_sign_in_id
  105. 19 return unless user_sign_in_id_does_not_match_token?
  106. 6 user.sign_in_user_id = sign_in_user_id_from_token
  107. end
  108. 4 def update_user_first_name
  109. 19 if first_name_from_token.blank?
  110. 1 log_message(:debug, user, "No first name in token")
  111. 1 return
  112. end
  113. 18 user.first_name = first_name_from_token
  114. end
  115. 4 def update_user_last_name
  116. 19 if last_name_from_token.blank?
  117. 1 log_message(:debug, user, "No last name in token")
  118. 1 return
  119. end
  120. 18 user.last_name = last_name_from_token
  121. end
  122. 4 def log_safe_user(user)
  123. 30 user.slice(
  124. "id",
  125. "state",
  126. "first_login_date_utc",
  127. "last_login_date_utc",
  128. "sign_in_user_id",
  129. "welcome_email_date_utc",
  130. "invite_date_utc",
  131. "accept_terms_date_utc",
  132. ).merge("email_md5" => obfuscate_email(user.email))
  133. end
  134. 4 def obfuscate_email(email)
  135. 36 Digest::MD5.hexdigest(email)
  136. end
  137. 4 def log_message(level, user, message, extra_attributes = {})
  138. 34 attributes = extra_attributes
  139. 34 attributes = attributes.merge(user: log_safe_user(user)) if user
  140. 34 logger.public_send(level) do
  141. 5 "#{message} #{attributes} "
  142. end
  143. end
  144. end

app/services/course_attribute_formatter_service.rb

100.0% lines covered

36 relevant lines. 36 lines covered and 0 lines missed.
    
  1. 2 class CourseAttributeFormatterService
  2. 2 def initialize(name:, value:)
  3. 24 @name = name
  4. 24 @value = value
  5. end
  6. 2 class << self
  7. 2 def call(**args)
  8. 24 new(args).call
  9. end
  10. end
  11. 2 def call
  12. 24 return age_range_value if age_range?
  13. 22 return qualification_value if qualification?
  14. 17 return study_mode_value if study_mode?
  15. 13 return entry_requirements_value if entry_requirements?
  16. 1 value
  17. end
  18. 2 private_class_method :new
  19. 2 private
  20. 2 attr_reader :name, :value
  21. 2 def age_range?
  22. 24 name == "age_range_in_years"
  23. end
  24. 2 def age_range_value
  25. 2 strip_underscores
  26. end
  27. 2 def qualification?
  28. 22 name == "qualification"
  29. end
  30. 2 def qualification_value
  31. 5 I18n.t("course.values.qualification.#{value}")
  32. end
  33. 2 def study_mode?
  34. 17 name == "study_mode"
  35. end
  36. 2 def study_mode_value
  37. 4 strip_underscores
  38. end
  39. 2 def entry_requirements?
  40. 13 %w[maths english science].include?(name)
  41. end
  42. 2 def entry_requirements_value
  43. 12 I18n.t("course.values.entry_requirements.#{value}")
  44. end
  45. 2 def strip_underscores
  46. 6 if value
  47. 4 value.tr("_", " ")
  48. else
  49. 2 "unknown"
  50. end
  51. end
  52. end

app/services/course_reporting_service.rb

100.0% lines covered

42 relevant lines. 42 lines covered and 0 lines missed.
    
  1. 3 class CourseReportingService
  2. 3 def initialize(courses_scope: Course)
  3. 11 @courses = courses_scope.distinct
  4. 11 @findable_courses = @courses.findable
  5. 11 @open_courses = @findable_courses.with_vacancies
  6. 11 @closed_courses = @findable_courses.where.not(id: @open_courses)
  7. end
  8. 3 class << self
  9. 3 def call(courses_scope:)
  10. 11 new(courses_scope: courses_scope).call
  11. end
  12. end
  13. 3 def call
  14. {
  15. 11 total: {
  16. all: @courses.count,
  17. non_findable: @courses.count - @findable_courses.count,
  18. all_findable: @findable_courses.count,
  19. },
  20. findable_total: {
  21. open: @open_courses.count,
  22. closed: @closed_courses.count,
  23. },
  24. provider_type: { **group_by_count(:provider_type) },
  25. program_type: { **group_by_count(:program_type) },
  26. study_mode: { **group_by_count(:study_mode) },
  27. qualification: { **group_by_count(:qualification) },
  28. is_send: { **group_by_count(:is_send) },
  29. subject: { **group_by_subject_count },
  30. }
  31. end
  32. 3 private_class_method :new
  33. 3 private
  34. 3 def group_by_subject_count
  35. 11 open = CourseSubject.where(course_id: @open_courses).group(:subject_id).count
  36. 11 closed = CourseSubject.where(course_id: @closed_courses).group(:subject_id).count
  37. {
  38. 11 open: Subject.active.map { |sub|
  39. 506 x = {}
  40. 506 x[sub.subject_name] = open[sub.id] || 0
  41. 506 x
  42. } .reduce({}, :merge),
  43. closed: Subject.active.map { |sub|
  44. 506 x = {}
  45. 506 x[sub.subject_name] = closed[sub.id] || 0
  46. 506 x
  47. } .reduce({}, :merge),
  48. }
  49. end
  50. 3 def group_by_count(column)
  51. 55 open = @open_courses.group(column).count
  52. 55 closed = @closed_courses.group(column).count
  53. 55 case column
  54. when :provider_type
  55. {
  56. 11 open: Provider.provider_types.map { |key, value|
  57. 33 x = {}
  58. 33 x[key.to_sym] = open[value] || 0
  59. 33 x
  60. } .reduce({}, :merge),
  61. closed: Provider.provider_types.map { |key, value|
  62. 33 x = {}
  63. 33 x[key.to_sym] = closed[value] || 0
  64. 33 x
  65. } .reduce({}, :merge),
  66. }
  67. when :program_type, :study_mode, :qualification
  68. {
  69. 33 open: Course.send(column.to_s.pluralize).map { |key, _value|
  70. 143 x = {}
  71. 143 x[key.to_sym] = open[key] || 0
  72. 143 x
  73. } .reduce({}, :merge),
  74. closed: Course.send(column.to_s.pluralize).map { |key, _value|
  75. 143 x = {}
  76. 143 x[key.to_sym] = closed[key] || 0
  77. 143 x
  78. } .reduce({}, :merge),
  79. }
  80. when :is_send
  81. {
  82. 11 open: { yes: open[true] || 0, no: open[false] || 0 },
  83. closed: { yes: closed[true] || 0, no: closed[false] || 0 },
  84. }
  85. end
  86. end
  87. end

app/services/course_search_service.rb

99.1% lines covered

111 relevant lines. 110 lines covered and 1 lines missed.
    
  1. 3 class CourseSearchService
  2. 3 def initialize(
  3. filter:,
  4. sort: nil,
  5. course_scope: Course
  6. )
  7. 89 @filter = filter || {}
  8. 89 @course_scope = course_scope
  9. 89 @sort = Set.new(sort&.split(","))
  10. end
  11. 3 class << self
  12. 3 def call(**args)
  13. 89 new(args).call
  14. end
  15. end
  16. 3 def call
  17. 89 scope = course_scope
  18. 89 scope = scope.with_salary if funding_filter_salary?
  19. 89 scope = scope.with_qualifications(qualifications) if qualifications.any?
  20. 89 scope = scope.with_vacancies if has_vacancies?
  21. 89 scope = scope.findable if findable?
  22. 89 scope = scope.with_study_modes(study_types) if study_types.any?
  23. 89 scope = scope.with_subjects(subject_codes) if subject_codes.any?
  24. 89 scope = scope.with_provider_name(provider_name) if provider_name.present?
  25. 89 scope = scope.with_send if send_courses_filter?
  26. 89 scope = scope.within(filter[:radius], origin: origin) if locations_filter?
  27. 89 scope = scope.with_funding_types(funding_types) if funding_types.any?
  28. 89 scope = scope.with_degree_grades(degree_grades) if degree_grades.any?
  29. 89 scope = scope.changed_since(filter[:updated_since]) if updated_since_filter?
  30. 89 scope = scope.provider_can_sponsor_visa if can_sponsor_visa_filter?
  31. # The 'where' scope will remove duplicates
  32. # An outer query is required in the event the provider name is present.
  33. # This prevents 'PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list'
  34. 89 outer_scope = Course.includes(
  35. :enrichments,
  36. :financial_incentives,
  37. course_subjects: [:subject],
  38. site_statuses: [:site],
  39. provider: %i[recruitment_cycle ucas_preferences],
  40. ).where(id: scope.select(:id))
  41. 89 if provider_name.present?
  42. 1 outer_scope = outer_scope
  43. .accredited_body_order(provider_name)
  44. .ascending_canonical_order
  45. 88 elsif sort_by_provider_ascending?
  46. 1 outer_scope = outer_scope.ascending_canonical_order
  47. 1 outer_scope = outer_scope.select("provider.provider_name", "course.*")
  48. 87 elsif sort_by_provider_descending?
  49. 1 outer_scope = outer_scope.descending_canonical_order
  50. 1 outer_scope = outer_scope.select("provider.provider_name", "course.*")
  51. 86 elsif sort_by_distance?
  52. 6 outer_scope = outer_scope.joins(courses_with_distance_from_origin)
  53. 6 outer_scope = outer_scope.joins(:provider)
  54. 6 outer_scope = outer_scope.select("course.*, distance, #{Course.sanitize_sql(distance_with_university_area_adjustment)}")
  55. outer_scope =
  56. 6 if expand_university?
  57. 2 outer_scope.order(:boosted_distance)
  58. else
  59. 4 outer_scope.order(:distance)
  60. end
  61. end
  62. 89 outer_scope
  63. end
  64. 3 private_class_method :new
  65. 3 private
  66. 3 def expand_university?
  67. 6 filter[:expand_university].to_s.downcase == "true"
  68. end
  69. 3 def distance_with_university_area_adjustment
  70. 6 university_provider_type = Provider.provider_types[:university]
  71. 6 university_location_area_radius = 10
  72. 6 <<~EOSQL.gsub(/\s+/m, " ").strip
  73. (CASE
  74. WHEN provider.provider_type = '#{university_provider_type}'
  75. THEN (distance - #{university_location_area_radius})
  76. ELSE distance
  77. END) as boosted_distance
  78. EOSQL
  79. end
  80. 3 def locatable_sites
  81. 12 site_statuses = SiteStatus.arel_table
  82. 12 sites = Site.arel_table
  83. # Create virtual table with sites and site statuses
  84. 12 site_statuses.join(sites).on(site_statuses[:site_id].eq(sites[:id]))
  85. .where(site_statuses_criteria(site_statuses))
  86. .where(has_been_geocoded_criteria(sites))
  87. .where(locatable_address_criteria(sites))
  88. end
  89. 3 def site_statuses_criteria(site_statuses)
  90. # Only running and published site statuses
  91. 12 running_and_published_criteria = site_statuses[:status].eq(SiteStatus.statuses[:running]).and(site_statuses[:publish].eq(SiteStatus.publishes[:published]))
  92. 12 if has_vacancies?
  93. # Only site statuses with vacancies
  94. running_and_published_criteria
  95. .and(site_statuses[:vac_status])
  96. .eq_any([
  97. SiteStatus.vac_statuses[:full_time_vacancies],
  98. SiteStatus.vac_statuses[:part_time_vacancies],
  99. SiteStatus.vac_statuses[:both_full_time_and_part_time_vacancies],
  100. ])
  101. else
  102. 12 running_and_published_criteria
  103. end
  104. end
  105. 3 def has_been_geocoded_criteria(sites)
  106. # we only want sites that have been geocoded
  107. 12 sites[:latitude].not_eq(nil).and(sites[:longitude].not_eq(nil))
  108. end
  109. 3 def locatable_address_criteria(sites)
  110. # only sites that have a locatable address
  111. # there are some sites with no address1 or postcode that cannot be
  112. # accurately geocoded. We don't want to return these as the closest site.
  113. # This should be removed once the data is fixed
  114. 12 sites[:address1].not_eq("").or(sites[:postcode].not_eq(""))
  115. end
  116. 3 def course_id_with_lowest_locatable_distance
  117. # select course_id and nearest site with shortest distance from origin
  118. # as courses may have multiple sites
  119. # this will remove duplicates by aggregating on course_id
  120. 12 origin_lat_long = Struct.new(:lat, :lng).new(origin[0].to_f, origin[1].to_f)
  121. 12 lowest_locatable_distance = Arel.sql("MIN#{Site.sanitize_sql(Site.distance_sql(origin_lat_long))} as distance")
  122. 12 locatable_sites.project(:course_id, lowest_locatable_distance).group(:course_id)
  123. end
  124. 3 def distance_table
  125. # form a temporary table with results
  126. 12 Arel::Nodes::TableAlias.new(
  127. Arel.sql(
  128. format("(%s)", course_id_with_lowest_locatable_distance.to_sql),
  129. ), "distances"
  130. )
  131. end
  132. 3 def courses_with_distance_from_origin
  133. # grab courses table and join with the above result set
  134. # so distances from origin are now available
  135. # we can then sort by distance from the given origin
  136. 6 courses_table = Course.arel_table
  137. 6 courses_table.join(distance_table).on(courses_table[:id].eq(distance_table[:course_id])).join_sources
  138. end
  139. 3 def locations_filter?
  140. 89 filter.key?(:latitude) &&
  141. filter.key?(:longitude) &&
  142. filter.key?(:radius)
  143. end
  144. 3 def sort_by_provider_ascending?
  145. 88 sort == Set["name", "provider.provider_name"]
  146. end
  147. 3 def sort_by_provider_descending?
  148. 87 sort == Set["-name", "-provider.provider_name"]
  149. end
  150. 3 def sort_by_distance?
  151. 86 sort == Set["distance"]
  152. end
  153. 3 def origin
  154. 25 [filter[:latitude], filter[:longitude]]
  155. end
  156. 3 attr_reader :sort, :filter, :course_scope
  157. 3 def funding_filter_salary?
  158. 89 filter[:funding] == "salary"
  159. end
  160. 3 def qualifications
  161. 90 return [] if filter[:qualification].blank?
  162. 2 filter[:qualification].split(",")
  163. end
  164. 3 def has_vacancies?
  165. 101 filter[:has_vacancies].to_s.downcase == "true"
  166. end
  167. 3 def findable?
  168. 89 filter[:findable].to_s.downcase == "true"
  169. end
  170. 3 def study_types
  171. 93 return [] if filter[:study_type].blank?
  172. 8 filter[:study_type].split(",")
  173. end
  174. 3 def funding_types
  175. 109 return [] if filter[:funding_type].blank?
  176. 40 filter[:funding_type].split(",")
  177. end
  178. 3 def degree_grades
  179. 93 return [] if filter[:degree_grade].blank?
  180. 8 filter[:degree_grade].split(",")
  181. end
  182. 3 def subject_codes
  183. 107 return [] if filter[:subjects].blank?
  184. 36 filter[:subjects].split(",")
  185. end
  186. 3 def provider_name
  187. 180 return [] if filter[:"provider.provider_name"].blank?
  188. 4 filter[:"provider.provider_name"]
  189. end
  190. 3 def send_courses_filter?
  191. 89 filter[:send_courses].to_s.downcase == "true"
  192. end
  193. 3 def updated_since_filter?
  194. 89 filter[:updated_since].present?
  195. end
  196. 3 def can_sponsor_visa_filter?
  197. 89 filter[:can_sponsor_visa].to_s.downcase == "true"
  198. end
  199. end

app/services/course_serializers_service.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 4 class CourseSerializersService
  2. 4 def initialize(
  3. course_serializer: API::V3::SerializableCourse,
  4. subject_serializer: API::V3::SerializableSubject,
  5. primary_subject_serializer: API::V3::SerializableSubject,
  6. secondary_subject_serializer: API::V3::SerializableSubject,
  7. modern_languages_subject_serializer: API::V3::SerializableSubject,
  8. further_education_subject_serializer: API::V3::SerializableSubject,
  9. site_status_serializer: API::V3::SerializableSiteStatus,
  10. site_serializer: API::V3::SerializableSite,
  11. provider_serializer: API::V3::SerializableProvider,
  12. recruitment_cycle_serializer: API::V3::SerializableRecruitmentCycle,
  13. subject_area_serializer: API::V3::SerializableSubjectArea
  14. )
  15. 72 @course_serializer = course_serializer
  16. 72 @subject_serializer = subject_serializer
  17. 72 @primary_subject_serializer = primary_subject_serializer
  18. 72 @secondary_subject_serializer = secondary_subject_serializer
  19. 72 @modern_languages_subject_serializer = modern_languages_subject_serializer
  20. 72 @further_education_subject_serializer = further_education_subject_serializer
  21. 72 @site_serializer = site_serializer
  22. 72 @site_status_serializer = site_status_serializer
  23. 72 @provider_serializer = provider_serializer
  24. 72 @recruitment_cycle_serializer = recruitment_cycle_serializer
  25. 72 @subject_area_serializer = subject_area_serializer
  26. end
  27. 4 def execute
  28. {
  29. 72 Course: @course_serializer,
  30. Subject: @subject_serializer,
  31. PrimarySubject: @primary_subject_serializer,
  32. SecondarySubject: @secondary_subject_serializer,
  33. ModernLanguagesSubject: @modern_languages_subject_serializer,
  34. FurtherEducationSubject: @further_education_subject_serializer,
  35. SiteStatus: @site_status_serializer,
  36. Site: @site_serializer,
  37. Provider: @provider_serializer,
  38. RecruitmentCycle: @recruitment_cycle_serializer,
  39. SubjectArea: @subject_area_serializer,
  40. }
  41. end
  42. end

app/services/courses/assign_program_type_service.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 class AssignProgramTypeService
  3. 4 def execute(funding_type, course)
  4. 95 case funding_type
  5. when "salary"
  6. 11 update_salaried_program(course)
  7. when "apprenticeship"
  8. 9 course.program_type = :pg_teaching_apprenticeship
  9. when "fee"
  10. 53 course.program_type = update_fee_program(course)
  11. end
  12. # NOTE: This looks like unwarranted side effects
  13. 95 course.save
  14. end
  15. 4 private
  16. 4 def update_salaried_program(course)
  17. 11 course.program_type = :school_direct_salaried_training_programme
  18. end
  19. 4 def update_fee_program(course)
  20. 53 if !course.self_accredited?
  21. 36 :school_direct_training_programme
  22. 17 elsif course.provider.scitt?
  23. 13 :scitt_programme
  24. else
  25. 4 :higher_education_programme
  26. end
  27. end
  28. end
  29. end

app/services/courses/assign_subjects_service.rb

100.0% lines covered

30 relevant lines. 30 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 class AssignSubjectsService
  3. 4 include ServicePattern
  4. 4 attr_reader :course, :subject_ids
  5. 4 def initialize(course:, subject_ids:)
  6. 161 @course = course
  7. 161 @subject_ids = subject_ids || []
  8. end
  9. 4 def call
  10. 161 course.errors.add(:subjects, :duplicate) if request_has_duplicate_subject_ids?
  11. 161 update_subjects
  12. 161 course.name = course.generate_name
  13. 161 course
  14. end
  15. 4 private
  16. 4 def subjects
  17. 154 @subjects ||= Subject.find(@subject_ids)
  18. end
  19. 4 def update_subjects
  20. 161 if course.further_education_course?
  21. 7 update_further_education_fields
  22. 7 course.subjects = [FurtherEducationSubject.instance]
  23. 7 course.course_subjects.first.position = 1
  24. else
  25. 154 course.subjects = subjects
  26. 154 subject_ids.each_with_index do |id, index|
  27. 286 course.course_subjects.select { |cs| cs.subject_id == id.to_i }.first.position = index
  28. end
  29. end
  30. end
  31. 4 def request_has_duplicate_subject_ids?
  32. 161 subject_ids.uniq.count != subject_ids.count
  33. end
  34. 4 def update_further_education_fields
  35. 7 course.funding_type = "fee"
  36. 7 course.english = "not_required"
  37. 7 course.maths = "not_required"
  38. 7 course.science = "not_required"
  39. end
  40. end
  41. end

app/services/courses/assignable_master_subject_service.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 class AssignableMasterSubjectService
  3. 4 def initialize(
  4. primary_subject: PrimarySubject,
  5. secondary_subject: SecondarySubject,
  6. further_education_subject: FurtherEducationSubject
  7. )
  8. 188 @primary_subject = primary_subject
  9. 188 @secondary_subject = secondary_subject
  10. 188 @further_education_subject = further_education_subject
  11. end
  12. 4 def execute(course:)
  13. 188 case course.level
  14. when "primary"
  15. 112 @primary_subject.includes(:financial_incentive).all
  16. when "secondary"
  17. 69 @secondary_subject.includes(:financial_incentive).all
  18. when "further_education"
  19. 7 @further_education_subject.includes(:financial_incentive).all
  20. end
  21. end
  22. end
  23. end

app/services/courses/assignable_subject_service.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 1 module Courses
  2. 1 class AssignableSubjectService
  3. 1 def initialize(
  4. primary_subject: PrimarySubject,
  5. secondary_subject: SecondarySubject,
  6. modern_language_subject: ModernLanguagesSubject,
  7. further_education_subject: FurtherEducationSubject,
  8. modern_languages_parent_subject: SecondarySubject.modern_languages
  9. )
  10. 3 @primary_subject = primary_subject
  11. 3 @secondary_subject = secondary_subject
  12. 3 @modern_language_subject = modern_language_subject
  13. 3 @further_education_subject = further_education_subject
  14. 3 @modern_languages_parent_subject = modern_languages_parent_subject
  15. end
  16. 1 def execute(course:)
  17. 3 case course.level
  18. when "primary"
  19. 1 @primary_subject.all
  20. when "secondary"
  21. 1 @secondary_subject.where.not(id: @modern_languages_parent_subject) + @modern_language_subject.all
  22. when "further_education"
  23. 1 @further_education_subject.all
  24. end
  25. end
  26. end
  27. end

app/services/courses/content_status_service.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 class ContentStatusService
  3. 4 def execute(enrichment:, recruitment_cycle:)
  4. 1201 if enrichment.blank?
  5. 821 if recruitment_cycle.next?
  6. 3 :rolled_over
  7. else
  8. 818 :empty
  9. end
  10. 380 elsif enrichment.published?
  11. 241 :published
  12. 139 elsif enrichment.withdrawn?
  13. 21 :withdrawn
  14. 118 elsif enrichment.has_been_published_before?
  15. 49 :published_with_unpublished_changes
  16. 69 elsif enrichment.rolled_over?
  17. 2 :rolled_over
  18. else
  19. 67 :draft
  20. end
  21. end
  22. end
  23. end

app/services/courses/copy.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 class Copy
  3. GCSE_FIELDS = [
  4. 4 ["Accept pending GCSE", "accept_pending_gcse"],
  5. ["Accept GCSE equivalency", "accept_gcse_equivalency"],
  6. ["Accept English GCSE equivalency", "accept_english_gcse_equivalency"],
  7. ["Accept Maths GCSE equivalency", "accept_maths_gcse_equivalency"],
  8. ["Additional GCSE equivalencies", "additional_gcse_equivalencies"],
  9. ].freeze
  10. SUBJECT_REQUIREMENTS_FIELDS = [
  11. 4 ["Additional degree subject requirements", "additional_degree_subject_requirements"],
  12. ["Degree subject requirements", "degree_subject_requirements"],
  13. ].freeze
  14. ABOUT_FIELDS = [
  15. 4 ["About the course", "about_course"],
  16. ["Interview process", "interview_process"],
  17. ["How school placements work", "how_school_placements_work"],
  18. ].freeze
  19. FEES_FIELDS = [
  20. 4 ["Course length", "course_length"],
  21. ["Fee for UK students", "fee_uk_eu"],
  22. ["Fee for international students", "fee_international"],
  23. ["Fee details", "fee_details"],
  24. ["Financial support", "financial_support"],
  25. ].freeze
  26. SALARY_FIELDS = [
  27. 4 ["Course length", "course_length"],
  28. ["Salary details", "salary_details"],
  29. ].freeze
  30. POST_2022_CYCLE_REQUIREMENTS_FIELDS = [
  31. 4 ["Personal qualities", "personal_qualities"],
  32. ["Other requirements", "other_requirements"],
  33. ].freeze
  34. PRE_2022_CYCLE_REQUIREMENTS_FIELDS = [
  35. 4 ["Qualifications needed", "required_qualifications"],
  36. ["Personal qualities", "personal_qualities"],
  37. ["Other requirements", "other_requirements"],
  38. ].freeze
  39. 4 def self.get_present_fields_in_source_course(fields, source_course, course)
  40. 4 fields.filter_map do |name, field|
  41. 10 source_value = source_course.enrichments.last[field]
  42. 10 if source_value.present?
  43. 7 course.enrichments.last[field] = source_value
  44. 7 [name, field]
  45. end
  46. end
  47. end
  48. 4 def self.get_boolean_fields(fields, source_course, course)
  49. 4 fields.select do |_name, field|
  50. 14 source_value = source_course[field]
  51. 14 course[field] = source_value if source_value.present?
  52. end
  53. end
  54. end
  55. end

app/services/courses/copy_to_provider_service.rb

100.0% lines covered

35 relevant lines. 35 lines covered and 0 lines missed.
    
  1. 2 module Courses
  2. 2 class CopyToProviderService
  3. 2 def initialize(sites_copy_to_course:, enrichments_copy_to_course:)
  4. 18 @sites_copy_to_course = sites_copy_to_course
  5. 18 @enrichments_copy_to_course = enrichments_copy_to_course
  6. end
  7. 2 def execute(course:, new_provider:, force: false)
  8. 18 return unless course.rollable? || force
  9. 18 return if course_code_already_exists_on_provider?(course: course, new_provider: new_provider)
  10. 12 new_course = nil
  11. 12 Course.transaction do
  12. 12 new_course = course.dup
  13. 12 new_course.uuid = nil
  14. 12 new_course.provider = new_provider
  15. 12 year_differential = new_course.recruitment_cycle.year.to_i - course.recruitment_cycle.year.to_i
  16. 12 new_course.applications_open_from = adjusted_applications_open_from_date(course, year_differential)
  17. 12 new_course.start_date = course.start_date + year_differential.year
  18. 12 new_course.subjects = course.subjects
  19. 12 new_course.save!(validate: false)
  20. 12 copy_latest_enrichment_to_course(course, new_course)
  21. 12 course.sites.each do |site|
  22. 3 new_site = new_provider.sites.find_by(code: site.code)
  23. 3 @sites_copy_to_course.execute(new_site: new_site, new_course: new_course) if new_site.present?
  24. end
  25. end
  26. 12 new_course
  27. end
  28. 2 private
  29. 2 def course_code_already_exists_on_provider?(course:, new_provider:)
  30. 18 new_provider.courses.with_discarded.where(course_code: course.course_code).any?
  31. end
  32. 2 def copy_latest_enrichment_to_course(course, new_course)
  33. 12 last_enrichment = course.enrichments.most_recent.first
  34. 12 return if last_enrichment.blank?
  35. 12 @enrichments_copy_to_course.execute(enrichment: last_enrichment, new_course: new_course)
  36. end
  37. 2 def adjusted_applications_open_from_date(course, year_differential)
  38. 12 next_cycle = RecruitmentCycle.next_recruitment_cycle
  39. 12 if course.applications_open_from + year_differential.year >= next_cycle.application_start_date
  40. 11 course.applications_open_from + year_differential.year
  41. else
  42. 1 next_cycle.application_start_date
  43. end
  44. end
  45. end
  46. end

app/services/courses/creation_service.rb

100.0% lines covered

36 relevant lines. 36 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 class CreationService
  3. 4 include ServicePattern
  4. 4 attr_reader :course_params, :provider, :next_available_course_code
  5. 4 def initialize(course_params:, provider:, next_available_course_code: false)
  6. 142 @course_params = course_params
  7. 142 @provider = provider
  8. 142 @next_available_course_code = next_available_course_code
  9. end
  10. 4 def call
  11. 142 build_new_course
  12. end
  13. 4 private
  14. 4 def new_course
  15. 142 @new_course ||= provider.courses.new
  16. end
  17. 4 def build_new_course
  18. 142 course = provider.courses.new
  19. 142 course.assign_attributes(course_attributes)
  20. 142 update_sites(course)
  21. 142 course.course_code = provider.next_available_course_code if next_available_course_code
  22. 142 AssignSubjectsService.call(course: course, subject_ids: subject_ids)
  23. 142 course.valid?(:new)
  24. 142 course.remove_carat_from_error_messages
  25. 142 course
  26. end
  27. 4 def course_attributes
  28. 142 @course_attributes ||= course_params.to_h.symbolize_keys.slice(*permitted_new_course_attributes)
  29. end
  30. 4 def permitted_new_course_attributes
  31. 142 @permitted_new_course_attributes ||= CoursePolicy.new(nil, new_course).permitted_new_course_attributes
  32. end
  33. 4 def sites
  34. 18 @sites ||= provider.sites.find(site_ids)
  35. end
  36. 4 def subject_ids
  37. 142 @subject_ids ||= course_params["subjects_ids"]
  38. end
  39. 4 def site_ids
  40. 198 @site_ids ||= course_params["sites_ids"]
  41. end
  42. 4 def update_sites(course)
  43. 142 return if site_ids.nil?
  44. 19 course.sites = sites if site_ids.any?
  45. 19 course.errors.add(:sites, message: "Select at least one location") if site_ids.empty?
  46. end
  47. end
  48. end

app/services/courses/fetch.rb

90.91% lines covered

11 relevant lines. 10 lines covered and 1 lines missed.
    
  1. 4 module Courses
  2. 4 class Fetch
  3. 4 class << self
  4. 4 def by_code(provider_code:, course_code:)
  5. 8 RecruitmentCycle.current.providers
  6. .find_by(provider_code: provider_code)
  7. .courses
  8. .find_by(course_code: course_code)
  9. end
  10. 4 def by_accrediting_provider(provider)
  11. # rubocop:disable Style/MultilineBlockChain
  12. # rubocop:disable Style/HashTransformValues
  13. 64 provider
  14. .courses
  15. .group_by do |course|
  16. 97 course.accrediting_provider&.provider_name || provider.provider_name
  17. rescue StandardError
  18. provider.provider_name
  19. end
  20. 57 .sort_by { |accrediting_provider, _| accrediting_provider.downcase }
  21. .to_h do |provider_name, courses|
  22. 154 [provider_name, courses.sort_by { |course| [course.name, course.course_code] }.map(&:decorate)]
  23. end
  24. # rubocop:enable Style/MultilineBlockChain
  25. # rubocop:enable Style/HashTransformValues
  26. end
  27. end
  28. end
  29. end

app/services/courses/generate_course_name_service.rb

100.0% lines covered

40 relevant lines. 40 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 class GenerateCourseNameService
  3. 4 def execute(course:)
  4. 192 subjects = course.subjects
  5. 192 title = if course.further_education_course?
  6. 9 further_education_title
  7. 183 elsif is_modern_lanuage_course?(subjects)
  8. 26 modern_language_title(subjects)
  9. else
  10. 157 generated_title(subjects)
  11. end
  12. 192 title += " (SEND)" if course.is_send?
  13. 192 title
  14. end
  15. 4 private
  16. 4 def is_modern_lanuage_course?(subjects)
  17. 348 subjects.any? { |s| s == SecondarySubject.modern_languages }
  18. end
  19. 4 def modern_language_title(subjects)
  20. 26 title = SecondarySubject.modern_languages.to_s
  21. 87 languages = subjects.select { |s| s.type == "ModernLanguagesSubject" }
  22. 61 languages = languages.reject { |language| language.subject_name.casecmp?("Modern languages (other)") }
  23. 26 return title if languages.empty? || languages.length >= 4
  24. 38 language_names = languages.map { |language| format_language_name(language) }
  25. 14 case language_names.length
  26. when 1
  27. 7 title + " (#{language_names[0]})"
  28. when 2
  29. 4 title + " (#{language_names.join(' and ')})"
  30. when 3
  31. 3 title + " (#{language_names.join(', ')})"
  32. end
  33. end
  34. 4 def generated_title(subjects)
  35. 157 return "" if subjects.empty?
  36. 270 subjects = subjects.map { |s| format_subject_name(s) }
  37. 132 if subjects.length == 1
  38. 126 subjects[0]
  39. else
  40. 6 "#{subjects[0]} with #{subjects[1]}"
  41. end
  42. end
  43. 4 def further_education_title
  44. 9 "Further education"
  45. end
  46. 4 def format_subject_name(subject)
  47. 138 if subject.subject_name.casecmp?("Communication and media studies")
  48. 2 "Media studies"
  49. else
  50. 136 subject.to_s
  51. end
  52. end
  53. 4 def format_language_name(language)
  54. 24 if language.subject_name.casecmp?("English as a second or other language")
  55. 3 "English"
  56. else
  57. 21 language.to_s
  58. end
  59. end
  60. end
  61. end

app/services/courses/validate_custom_age_range_service.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 4 module Courses
  2. 4 class ValidateCustomAgeRangeService
  3. 4 def execute(age_range_in_years, course)
  4. 1843 valid_age_range_regex = Regexp.new(/^(?<from>\d{1,2})_to_(?<to>\d{1,2})$/)
  5. 1843 if valid_age_range_regex.match(age_range_in_years)
  6. 1748 from_age = get_ages(age_range_in_years, valid_age_range_regex)["from"]
  7. 1748 to_age = get_ages(age_range_in_years, valid_age_range_regex)["to"]
  8. 1748 if from_age_invalid?(from_age) || to_age_invalid?(to_age)
  9. 2 course.errors.add(:age_range_in_years, "^Age range must be a school age")
  10. 1746 elsif to_age - from_age < 4
  11. 1 course.errors.add(:age_range_in_years, "^Age range must cover at least 4 years")
  12. end
  13. else
  14. 95 course.errors.add(:age_range_in_years, "^Enter an age range")
  15. end
  16. end
  17. 4 private
  18. 4 def get_ages(age_range_in_years, valid_age_range_regex)
  19. 10488 valid_age_range_regex.match(age_range_in_years)&.named_captures&.transform_values { |year| year.to_i }
  20. end
  21. 4 def from_age_invalid?(from_age)
  22. 1748 from_age < 3 || from_age > 15
  23. end
  24. 4 def to_age_invalid?(to_age)
  25. 1747 to_age < 7 || to_age > 19
  26. end
  27. end
  28. end

app/services/csv_imports/fake_providers_import.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. 1 require "csv"
  2. 1 module CSVImports
  3. 1 class FakeProvidersImport
  4. 1 attr_reader :results
  5. 1 def initialize(path_to_csv)
  6. 5 @results = []
  7. 5 @path_to_csv = path_to_csv
  8. end
  9. 1 def execute
  10. 5 CSV.foreach(@path_to_csv, headers: true) do |row|
  11. 8 provider_name = row["name"]
  12. 8 provider_code = row["code"]
  13. 8 provider_type = row["type"]
  14. 8 is_accredited_body = ActiveModel::Type::Boolean.new.cast(row["accredited_body"])
  15. 8 service = Providers::CreateFakeProviderService.new(
  16. recruitment_cycle: RecruitmentCycle.current,
  17. provider_name: provider_name,
  18. provider_code: provider_code,
  19. provider_type: provider_type,
  20. is_accredited_body: is_accredited_body,
  21. )
  22. 8 @results << if service.execute
  23. 7 "Created provider #{provider_name}."
  24. else
  25. 1 service.errors.join(" ")
  26. end
  27. end
  28. end
  29. end
  30. end

app/services/enrichments/copy_to_course_service.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 2 module Enrichments
  2. 2 class CopyToCourseService
  3. 2 def execute(enrichment:, new_course:)
  4. 5 new_enrichment = enrichment.dup
  5. 5 new_enrichment.last_published_timestamp_utc = nil
  6. 5 new_enrichment.rolled_over!
  7. 5 new_course.enrichments << new_enrichment
  8. end
  9. end
  10. end

app/services/exports/accredited_course_list.rb

90.0% lines covered

20 relevant lines. 18 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "csv"
  3. 1 module Exports
  4. 1 class AccreditedCourseList
  5. 1 def initialize(courses)
  6. 2 @data_for_export = format_courses(courses)
  7. end
  8. 1 def data
  9. 2 CSV.generate(headers: data_for_export.first.keys, write_headers: true) do |csv|
  10. 2 data_for_export.each do |course_csv_row|
  11. 2 csv << course_csv_row
  12. end
  13. end
  14. end
  15. 1 def filename
  16. "courses-#{Time.zone.today}.csv"
  17. end
  18. 1 private
  19. 1 attr_reader :data_for_export
  20. 1 def format_courses(courses)
  21. 2 courses
  22. .map(&:decorate)
  23. .flat_map do |c|
  24. base_data = {
  25. 2 "Provider code" => c.provider.provider_code,
  26. "Provider" => c.provider.provider_name,
  27. "Course code" => c.course_code,
  28. "Course" => c.name,
  29. "Study mode" => c.study_mode&.humanize,
  30. "Programme type" => c.program_type&.humanize,
  31. "Qualification" => c.outcome,
  32. "Status" => c.content_status&.to_s&.humanize,
  33. "View on Find" => c.find_url,
  34. "Applications open from" => I18n.l(c.applications_open_from&.to_date),
  35. 2 "Vacancies" => c.has_vacancies? ? "Yes" : "No",
  36. }
  37. 2 if c.sites
  38. 2 base_data.merge({ "Campus Codes" => c.sites.pluck(:code).join(" ") })
  39. else
  40. base_data
  41. end
  42. end
  43. end
  44. end
  45. end

app/services/feature_service.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module FeatureService
  3. 4 class << self
  4. 4 def require(feature_name)
  5. 4 unless enabled?(feature_name)
  6. 2 raise "Feature #{feature_name} is disabled"
  7. end
  8. 2 true
  9. end
  10. 4 def enabled?(feature_name)
  11. 18574 return false if Settings.features.blank?
  12. 18572 segments = feature_name.to_s.split(".")
  13. 39259 segments.reduce(Settings.features) { |config, segment| config[segment] }
  14. end
  15. end
  16. end

app/services/generate_and_send_magic_link_service.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 2 class GenerateAndSendMagicLinkService
  2. 2 class << self
  3. 2 def call(user:)
  4. 3 new.call(user: user)
  5. end
  6. end
  7. 2 def call(user:)
  8. 3 generate_magic_link_token(user)
  9. 3 send_magic_link(user)
  10. end
  11. 2 private
  12. 2 def generate_magic_link_token(user)
  13. 3 user.magic_link_token = SecureRandom.uuid
  14. 3 user.magic_link_token_sent_at = Time.now.utc
  15. 3 user.save!
  16. end
  17. 2 def send_magic_link(user)
  18. 3 MagicLinkEmailMailer
  19. .magic_link_email(user)
  20. .deliver_later
  21. end
  22. end

app/services/geocoder_service.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 2 class GeocoderService
  2. 2 def self.geocode(obj:, force: false)
  3. 5 return if obj.full_address.blank?
  4. 4 result = Geokit::Geocoders::GoogleGeocoder.geocode(obj.full_address, bias: "gb")
  5. 4 if result&.success == true
  6. 3 obj.latitude = result.latitude
  7. 3 obj.longitude = result.longitude
  8. 3 region_code = COUNTY_TO_REGION_CODE[result.district]
  9. 3 obj.region_code = region_code if region_code.present?
  10. 3 obj.save!(validate: !force)
  11. end
  12. end
  13. end

app/services/notification_service/course_published.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 3 module NotificationService
  2. 3 class CoursePublished
  3. 3 include ServicePattern
  4. 3 def initialize(course:)
  5. 8 @course = course
  6. end
  7. 3 def call
  8. 8 return false unless notify_accredited_body?
  9. 4 return false unless course.in_current_cycle?
  10. 3 users.each do |user|
  11. 3 CoursePublishEmailMailer.course_publish_email(
  12. course,
  13. user,
  14. ).deliver_later
  15. end
  16. end
  17. 3 private
  18. 3 attr_reader :course
  19. 3 def users
  20. 3 User.course_publish_subscribers(course.accredited_body_code)
  21. end
  22. 3 def notify_accredited_body?
  23. 8 return false if course.self_accredited?
  24. 7 return false unless course.findable?
  25. 4 true
  26. end
  27. end
  28. end

app/services/notification_service/course_sites_updated.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. 2 module NotificationService
  2. 2 class CourseSitesUpdated
  3. 2 include ServicePattern
  4. 2 def initialize(course:, previous_site_names:, updated_site_names:)
  5. 7 @course = course
  6. 7 @previous_site_names = previous_site_names
  7. 7 @updated_site_names = updated_site_names
  8. end
  9. 2 def call
  10. 7 return unless course_needs_to_notify?
  11. 4 users_to_notify.each do |user|
  12. 3 CourseSitesUpdateEmailMailer.course_sites_update_email(
  13. course: course,
  14. previous_site_names: previous_site_names,
  15. updated_site_names: updated_site_names,
  16. recipient: user,
  17. ).deliver_later
  18. end
  19. end
  20. 2 private
  21. 2 attr_reader :course, :previous_site_names, :updated_site_names
  22. 2 def users_to_notify
  23. 4 User.joins(:user_notifications).merge(
  24. UserNotification.course_update_notification_requests(course.accredited_body_code),
  25. )
  26. end
  27. 2 def course_needs_to_notify?
  28. 7 course.findable? &&
  29. !course.self_accredited? &&
  30. course.in_current_cycle?
  31. end
  32. end
  33. end

app/services/notification_service/course_subjects_updated.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. 2 module NotificationService
  2. 2 class CourseSubjectsUpdated
  3. 2 include ServicePattern
  4. 2 def initialize(
  5. course:,
  6. previous_subject_names:,
  7. previous_course_name:
  8. )
  9. 9 @course = course
  10. 9 @previous_subject_names = previous_subject_names
  11. 9 @previous_course_name = previous_course_name
  12. end
  13. 2 def call
  14. 9 return false unless notify_accredited_body?
  15. 4 return false unless course.in_current_cycle?
  16. 3 users.each do |user|
  17. 3 CourseSubjectsUpdatedEmailMailer.course_subjects_updated_email(
  18. course: course,
  19. previous_subject_names: previous_subject_names,
  20. previous_course_name: previous_course_name,
  21. recipient: user,
  22. ).deliver_later
  23. end
  24. end
  25. 2 private
  26. 2 attr_reader :course, :previous_subject_names, :previous_course_name
  27. 2 def users
  28. 3 User.course_update_subscribers(course.accredited_body_code)
  29. end
  30. 2 def notify_accredited_body?
  31. 9 return false if course.self_accredited?
  32. 8 return false unless course.findable?
  33. 4 true
  34. end
  35. end
  36. end

app/services/notification_service/course_updated.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 2 module NotificationService
  2. 2 class CourseUpdated
  3. 2 include ServicePattern
  4. 2 def initialize(course:)
  5. 6 @course = course
  6. end
  7. 2 def call
  8. 6 return unless course_needs_to_notify?
  9. 3 notifiable_changes.each do |updated_attribute|
  10. 10 original_value, updated_value = course.saved_changes[updated_attribute]
  11. 10 users.each do |user|
  12. 20 CourseUpdateEmailMailer.course_update_email(
  13. course: course,
  14. attribute_name: updated_attribute,
  15. original_value: original_value,
  16. updated_value: updated_value,
  17. recipient: user,
  18. ).deliver_later
  19. end
  20. end
  21. end
  22. 2 private
  23. 2 attr_reader :course
  24. 2 def notifiable_changes
  25. 7 course.saved_changes.keys & course.update_notification_attributes
  26. end
  27. 2 def users
  28. 10 User.course_update_subscribers(course.accredited_body_code)
  29. end
  30. 2 def course_needs_to_notify?
  31. 6 course.findable? &&
  32. !course.self_accredited? &&
  33. notifiable_changes.any? &&
  34. course.in_current_cycle?
  35. end
  36. end
  37. end

app/services/notification_service/course_vacancies_updated.rb

100.0% lines covered

40 relevant lines. 40 lines covered and 0 lines missed.
    
  1. 2 module NotificationService
  2. 2 class CourseVacanciesUpdated
  3. 2 include ServicePattern
  4. 2 def initialize(course:, vacancy_statuses:)
  5. 16 @course = course
  6. 16 @vacancy_statuses = vacancy_statuses
  7. end
  8. 2 def call
  9. 16 return false unless notify_accredited_body?
  10. 14 if all_vacancies_closed?
  11. 6 send_course_vacancies_updated_notification(vacancies_filled: true)
  12. 8 elsif all_vacancies_open?
  13. 5 send_course_vacancies_updated_notification(vacancies_filled: false)
  14. else
  15. 3 send_course_vacancies_partially_updated_notification
  16. end
  17. end
  18. 2 private
  19. 2 attr_reader :course, :vacancy_statuses
  20. 2 def send_course_vacancies_updated_notification(vacancies_filled:)
  21. 11 users.each do |user|
  22. 8 CourseVacancies::UpdatedMailer.fully_updated(
  23. course: course,
  24. user: user,
  25. datetime: DateTime.now,
  26. vacancies_filled: vacancies_filled,
  27. ).deliver_later
  28. end
  29. end
  30. 2 def send_course_vacancies_partially_updated_notification
  31. 3 users.each do |user|
  32. 2 CourseVacancies::UpdatedMailer.partially_updated(
  33. course: course,
  34. user: user,
  35. datetime: DateTime.now,
  36. vacancies_opened: vacancies_opened,
  37. vacancies_closed: vacancies_closed,
  38. ).deliver_later
  39. end
  40. end
  41. 2 def all_vacancies_open?
  42. 8 vacancy_statuses.all? do |vacancy_status|
  43. 11 vacancy_status[:status] == "full_time_vacancies" ||
  44. vacancy_status[:status] == "both_full_time_and_part_time_vacancies" ||
  45. vacancy_status[:status] == "part_time_vacancies"
  46. end
  47. end
  48. 2 def all_vacancies_closed?
  49. 33 vacancy_statuses.all? { |vacancy_status| vacancy_status[:status] == "no_vacancies" }
  50. end
  51. 2 def vacancies_closed
  52. 2 vacancy_statuses
  53. 4 .select { |vacancy_status| vacancy_status[:status] == "no_vacancies" }
  54. 2 .map { |vacancy_status| SiteStatus.find(vacancy_status[:id]).site.location_name }
  55. end
  56. 2 def vacancies_opened
  57. 2 vacancy_statuses
  58. 4 .select { |vacancy_status| vacancy_status[:status] == "full_time_vacancies" || vacancy_status[:status] == "part_time_vacancies" || vacancy_status[:status] == "both_full_time_and_part_time_vacancies" }
  59. 2 .map { |vacancy_status| SiteStatus.find(vacancy_status[:id]).site.location_name }
  60. end
  61. 2 def users
  62. 14 User.course_publish_subscribers(course.accredited_body_code)
  63. end
  64. 2 def notify_accredited_body?
  65. 16 return false if course.self_accredited?
  66. 15 return false unless course.in_current_cycle?
  67. 14 true
  68. end
  69. end
  70. end

app/services/notification_service/course_withdrawn.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. 3 module NotificationService
  2. 3 class CourseWithdrawn
  3. 3 include ServicePattern
  4. 3 def initialize(course:)
  5. 6 @course = course
  6. end
  7. 3 def call
  8. 6 return false unless notify_accredited_body?
  9. 5 return false unless course.in_current_cycle?
  10. 4 users.each do |user|
  11. 4 CourseWithdrawEmailMailer.course_withdraw_email(
  12. course,
  13. user,
  14. DateTime.now,
  15. ).deliver_later
  16. end
  17. end
  18. 3 private
  19. 3 attr_reader :course
  20. 3 def users
  21. 4 User.course_publish_subscribers(course.accredited_body_code)
  22. end
  23. 3 def notify_accredited_body?
  24. 6 return false if course.self_accredited?
  25. 5 true
  26. end
  27. end
  28. end

app/services/performance_dashboard_service.rb

0.0% lines covered

86 relevant lines. 0 lines covered and 86 lines missed.
    
  1. class PerformanceDashboardService
  2. include ActionView::Helpers::NumberHelper
  3. include ServicePattern
  4. attr_accessor :recruitment_cycle
  5. def initialize(
  6. recruitment_cycle: RecruitmentCycle.current_recruitment_cycle
  7. )
  8. @recruitment_cycle = recruitment_cycle
  9. end
  10. def call
  11. self
  12. end
  13. def total_providers
  14. number_with_delimiter(reporting[:providers][:total][:all])
  15. end
  16. def total_courses
  17. number_with_delimiter(reporting[:courses][:total][:all_findable])
  18. end
  19. def total_users
  20. number_with_delimiter(reporting[:publish][:users][:total][:all])
  21. end
  22. def total_allocations
  23. number_with_delimiter(reporting[:allocations][:current][:total][:allocations])
  24. end
  25. def providers_published_courses
  26. number_with_delimiter(reporting[:providers][:training_providers][:findable_total][:open])
  27. end
  28. def providers_unpublished_courses
  29. number_with_delimiter(reporting[:providers][:training_providers][:findable_total][:closed])
  30. end
  31. def providers_accredited_bodies
  32. number_with_delimiter(reporting[:providers][:training_providers][:accredited_body][:open][:accredited_body])
  33. end
  34. def courses_total_open
  35. number_with_delimiter(reporting[:courses][:findable_total][:open])
  36. end
  37. def courses_total_closed
  38. number_with_delimiter(reporting[:courses][:findable_total][:closed])
  39. end
  40. def courses_total_draft
  41. number_with_delimiter(reporting[:courses][:total][:non_findable])
  42. end
  43. def allocations_requests(recruitment_cycle)
  44. number_with_delimiter(reporting[:allocations][recruitment_cycle.to_sym][:total][:allocations])
  45. end
  46. def allocations_number_of_places(recruitment_cycle)
  47. number_with_delimiter(reporting[:allocations][recruitment_cycle.to_sym][:total][:number_of_places])
  48. end
  49. def allocations_accredited_bodies(recruitment_cycle)
  50. number_with_delimiter(reporting[:allocations][recruitment_cycle.to_sym][:total][:distinct_accredited_bodies])
  51. end
  52. def allocations_providers(recruitment_cycle)
  53. number_with_delimiter(reporting[:allocations][recruitment_cycle.to_sym][:total][:distinct_providers])
  54. end
  55. def users_active
  56. number_with_delimiter(reporting[:publish][:users][:total][:active_users])
  57. end
  58. def users_not_active
  59. number_with_delimiter(reporting[:publish][:users][:total][:non_active_users])
  60. end
  61. def users_active_30_days
  62. number_with_delimiter(reporting[:publish][:users][:recent_active_users])
  63. end
  64. def rollover_total
  65. reporting[:rollover][:total]
  66. end
  67. def published_courses
  68. number_with_delimiter(rollover_total[:published_courses])
  69. end
  70. def new_courses_published
  71. number_with_delimiter(rollover_total[:new_courses_published])
  72. end
  73. def deleted_courses
  74. number_with_delimiter(rollover_total[:deleted_courses])
  75. end
  76. def existing_courses_in_draft
  77. number_with_delimiter(rollover_total[:existing_courses_in_draft])
  78. end
  79. def existing_courses_in_review
  80. number_with_delimiter(rollover_total[:existing_courses_in_review])
  81. end
  82. private
  83. def reporting
  84. @reporting ||= StatisticService.reporting(recruitment_cycle: recruitment_cycle)
  85. end
  86. end

app/services/provider_enrichments/rollover_enrichment_to_provider_service.rb

0.0% lines covered

10 relevant lines. 0 lines covered and 10 lines missed.
    
  1. module ProviderEnrichments
  2. class RolloverEnrichmentToProviderService
  3. def execute(enrichment:, new_provider:)
  4. new_enrichment = enrichment.dup
  5. new_enrichment.last_published_at = nil
  6. new_enrichment.provider = new_provider
  7. new_enrichment.rolled_over!
  8. end
  9. end
  10. end

app/services/provider_reporting_service.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. 3 class ProviderReportingService
  2. 3 def initialize(providers_scope: Provider)
  3. 11 @providers = providers_scope.distinct
  4. 11 @training_providers = @providers.where(id: Course.findable.select(:provider_id))
  5. 11 @open_providers = @providers.where(id: Course.with_vacancies.select(:provider_id))
  6. 11 @closed_providers = @training_providers.where.not(id: @open_providers)
  7. end
  8. 3 class << self
  9. 3 def call(providers_scope:)
  10. 11 new(providers_scope: providers_scope).call
  11. end
  12. end
  13. 3 def call
  14. {
  15. 11 total: {
  16. all: @providers.count,
  17. non_training_providers: @providers.count - @training_providers.count,
  18. training_providers: @training_providers.count,
  19. },
  20. training_providers: {
  21. findable_total: {
  22. open: @open_providers.count,
  23. closed: @closed_providers.count,
  24. },
  25. accredited_body: { **group_by_count(:accrediting_provider) },
  26. provider_type: { **group_by_count(:provider_type) },
  27. region_code: { **group_by_count(:region_code) },
  28. },
  29. }
  30. end
  31. 3 private_class_method :new
  32. 3 private
  33. 3 def group_by_count(column)
  34. 33 open = @open_providers.group(column).count
  35. 33 closed = @closed_providers.group(column).count
  36. {
  37. 33 open: Provider.send(column.to_s.pluralize).map { |key, _value|
  38. 187 x = {}
  39. 187 x[key.to_sym] = open[key] || 0
  40. 187 x
  41. } .reduce({}, :merge),
  42. closed: Provider.send(column.to_s.pluralize).map { |key, _value|
  43. 187 x = {}
  44. 187 x[key.to_sym] = closed[key] || 0
  45. 187 x
  46. } .reduce({}, :merge),
  47. }
  48. end
  49. end

app/services/providers/copy_to_recruitment_cycle_service.rb

100.0% lines covered

42 relevant lines. 42 lines covered and 0 lines missed.
    
  1. 1 module Providers
  2. 1 class CopyToRecruitmentCycleService
  3. 1 attr :logger
  4. 1 def initialize(copy_course_to_provider_service:, copy_site_to_provider_service:, logger: nil)
  5. 14 @copy_course_to_provider_service = copy_course_to_provider_service
  6. 14 @copy_site_to_provider_service = copy_site_to_provider_service
  7. 14 @logger = logger || Logger.new("/dev/null")
  8. end
  9. 1 def execute(provider:, new_recruitment_cycle:, force: false)
  10. 14 providers_count = 0
  11. 14 sites_count = 0
  12. 14 courses_count = 0
  13. 14 if provider.rollable? || force
  14. 14 ActiveRecord::Base.transaction do
  15. 14 rolled_over_provider = new_recruitment_cycle.providers.find_by(provider_code: provider.provider_code)
  16. 14 if rolled_over_provider.nil?
  17. 11 providers_count = 1
  18. 11 rolled_over_provider = provider.dup
  19. 11 rolled_over_provider.organisations << provider.organisations
  20. 11 rolled_over_provider.ucas_preferences = provider.ucas_preferences.dup
  21. 11 rolled_over_provider.contacts << provider.contacts.map(&:dup)
  22. 11 rolled_over_provider.recruitment_cycle = new_recruitment_cycle
  23. 11 rolled_over_provider.skip_geocoding = true
  24. 11 rolled_over_provider.users << provider.users
  25. 11 rolled_over_provider.save!
  26. end
  27. # Order is important here. Sites should be copied over before courses
  28. # so that courses can link up to the correct sites in the new provider.
  29. 14 sites_count = copy_sites_to_new_provider(provider, rolled_over_provider)
  30. 14 courses_count = copy_courses_to_new_provider(provider, rolled_over_provider)
  31. end
  32. end
  33. {
  34. 13 providers: providers_count,
  35. sites: sites_count,
  36. courses: courses_count,
  37. }
  38. end
  39. 1 private
  40. 1 def copy_courses_to_new_provider(provider, new_provider)
  41. 14 courses_count = 0
  42. 14 provider.courses.each do |course|
  43. 14 new_course = @copy_course_to_provider_service.execute(course: course, new_provider: new_provider)
  44. 13 courses_count += 1 if new_course.present?
  45. rescue Exception # rubocop: disable Lint/RescueException
  46. 1 logger&.fatal "error trying to copy course #{course.course_code}"
  47. 1 raise
  48. end
  49. 13 courses_count
  50. end
  51. 1 def copy_sites_to_new_provider(provider, new_provider)
  52. 14 sites_count = 0
  53. 14 provider.sites.each do |site|
  54. 14 new_site = @copy_site_to_provider_service.execute(site: site, new_provider: new_provider)
  55. 14 sites_count += 1 if new_site.present?
  56. end
  57. 14 sites_count
  58. end
  59. end
  60. end

app/services/providers/create_fake_provider_service.rb

100.0% lines covered

31 relevant lines. 31 lines covered and 0 lines missed.
    
  1. 2 module Providers
  2. 2 class CreateFakeProviderService
  3. 2 attr_reader :errors
  4. DEFAULT_PROVIDER_ATTRIBUTES = {
  5. 2 address1: "1 Test Street",
  6. address3: "Town",
  7. address4: "County",
  8. postcode: "M1 1JG",
  9. region_code: "north_west",
  10. ukprn: "12345678",
  11. }.freeze
  12. 2 def initialize(recruitment_cycle:, provider_name:, provider_code:, provider_type:, is_accredited_body:)
  13. 13 raise "Can only be run in sandbox or development" unless Rails.env.sandbox? || Rails.env.development? || Rails.env.test?
  14. 13 @recruitment_cycle = recruitment_cycle
  15. 13 @provider_name = provider_name
  16. 13 @provider_code = provider_code
  17. 13 @provider_type = provider_type
  18. 13 @is_accredited_body = is_accredited_body
  19. 13 @errors = []
  20. end
  21. 2 def execute
  22. 13 return false if provider_already_exists?
  23. 11 return false if attempting_to_make_lead_school_accredited_body?
  24. 10 provider = @recruitment_cycle.providers.build({
  25. provider_name: @provider_name,
  26. provider_code: @provider_code,
  27. provider_type: @provider_type,
  28. 10 accrediting_provider: @is_accredited_body ? "accredited_body" : "not_an_accredited_body",
  29. }.merge(DEFAULT_PROVIDER_ATTRIBUTES))
  30. 10 organisation = Organisation.new(name: @provider_name)
  31. 10 organisation.providers << provider
  32. 10 organisation.save!
  33. 10 provider.sites.create!(
  34. location_name: "Site 1",
  35. address1: provider.address1,
  36. address2: provider.address2,
  37. address3: provider.address3,
  38. address4: provider.address4,
  39. postcode: provider.postcode,
  40. region_code: provider.region_code,
  41. urn: Faker::Number.number(digits: [5, 6].sample),
  42. )
  43. 10 true
  44. end
  45. 2 private
  46. 2 def provider_already_exists?
  47. 13 if @recruitment_cycle.providers.exists?(provider_code: @provider_code)
  48. 2 errors << "Provider #{@provider_name} (#{@provider_code}) already exists."
  49. 2 true
  50. else
  51. 11 false
  52. end
  53. end
  54. 2 def attempting_to_make_lead_school_accredited_body?
  55. 11 if @provider_type == "lead_school" && @is_accredited_body
  56. 1 errors << "Provider #{@provider_name} (#{@provider_code}) cannot be both a lead school and an accredited body."
  57. end
  58. end
  59. end
  60. end

app/services/providers/generate_course_code_service.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 4 module Providers
  2. 4 class GenerateCourseCodeService
  3. 4 def execute
  4. 7 "#{valid_letters.sample}#{valid_number}"
  5. end
  6. 4 private
  7. 4 def valid_letters
  8. 10 ("A".."Z").to_a - %w[O I]
  9. end
  10. 4 def valid_number
  11. 7 [*0..999].sample.to_s.rjust(3, "0")
  12. end
  13. end
  14. end

app/services/providers/generate_unique_course_code_service.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 3 module Providers
  2. 3 class GenerateUniqueCourseCodeService
  3. 3 def initialize(generate_course_code_service:)
  4. 9 @generate_course_code_service = generate_course_code_service
  5. end
  6. 3 def execute(existing_codes:)
  7. 9 code = nil
  8. 9 code = @generate_course_code_service.execute while code.nil? || code.in?(existing_codes)
  9. 9 code
  10. end
  11. end
  12. end

app/services/providers/visa_sponsorship_service.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 2 module Providers
  2. 2 class VisaSponsorshipService
  3. 2 VISA_SPONSORSHIP_INTRODUCED_IN = 2022
  4. 2 def initialize(provider)
  5. 8 @provider = provider
  6. end
  7. 2 def visa_sponsorship_enabled?
  8. 8 @provider.recruitment_cycle_year.to_i >= VISA_SPONSORSHIP_INTRODUCED_IN
  9. end
  10. end
  11. end

app/services/publish/course_creation_step_service.rb

80.0% lines covered

40 relevant lines. 32 lines covered and 8 lines missed.
    
  1. 4 module Publish
  2. 4 class CourseCreationStepService
  3. 4 def execute(current_step:, course:)
  4. 162 workflow_steps = get_workflow_steps(course)
  5. {
  6. 162 next: get_next_step(workflow_steps, current_step),
  7. previous: get_previous_step(workflow_steps, current_step),
  8. }
  9. end
  10. 4 def get_next_step(steps, current_step)
  11. 162 next_step_index = steps.find_index(current_step).next
  12. 162 steps[next_step_index]
  13. end
  14. 4 def get_previous_step(steps, current_step)
  15. 162 previous_step_index = steps.find_index(current_step).pred
  16. 162 steps[previous_step_index]
  17. end
  18. 4 def get_workflow_steps(course)
  19. 162 workflows_for_2022_cycle_onwards(course)
  20. end
  21. 4 private
  22. 4 def workflows_for_2022_cycle_onwards(course)
  23. 162 if course.is_further_education?
  24. 3 new_further_education_workflow_steps
  25. 159 elsif course.is_uni_or_scitt?
  26. 39 new_uni_or_scitt_workflow_steps
  27. 120 elsif course.is_school_direct?
  28. 120 new_school_direct_workflow_steps
  29. end
  30. end
  31. 4 def workflows_for_current_cycle(course)
  32. if course.is_further_education?
  33. further_education_workflow_steps
  34. elsif course.is_uni_or_scitt?
  35. uni_or_scitt_workflow_steps
  36. elsif course.is_school_direct?
  37. school_direct_workflow_steps
  38. end
  39. end
  40. 4 def school_direct_workflow_steps
  41. %i[
  42. courses_list
  43. level
  44. subjects
  45. modern_languages
  46. age_range
  47. outcome
  48. fee_or_salary
  49. full_or_part_time
  50. location
  51. accredited_body
  52. entry_requirements
  53. applications_open
  54. start_date
  55. confirmation
  56. ]
  57. end
  58. 4 def uni_or_scitt_workflow_steps
  59. %i[
  60. courses_list
  61. level
  62. subjects
  63. modern_languages
  64. age_range
  65. outcome
  66. apprenticeship
  67. full_or_part_time
  68. location
  69. entry_requirements
  70. applications_open
  71. start_date
  72. confirmation
  73. ]
  74. end
  75. 4 def further_education_workflow_steps
  76. 3 %i[
  77. courses_list
  78. level
  79. outcome
  80. full_or_part_time
  81. location
  82. applications_open
  83. start_date
  84. confirmation
  85. ]
  86. end
  87. 4 def new_school_direct_workflow_steps
  88. 120 %i[
  89. courses_list
  90. level
  91. subjects
  92. modern_languages
  93. age_range
  94. outcome
  95. fee_or_salary
  96. full_or_part_time
  97. location
  98. accredited_body
  99. applications_open
  100. start_date
  101. confirmation
  102. ]
  103. end
  104. 4 def new_uni_or_scitt_workflow_steps
  105. 39 %i[
  106. courses_list
  107. level
  108. subjects
  109. modern_languages
  110. age_range
  111. outcome
  112. apprenticeship
  113. full_or_part_time
  114. location
  115. applications_open
  116. start_date
  117. confirmation
  118. ]
  119. end
  120. 4 def new_further_education_workflow_steps
  121. 3 further_education_workflow_steps
  122. end
  123. end
  124. end

app/services/publish_reporting_service.rb

100.0% lines covered

46 relevant lines. 46 lines covered and 0 lines missed.
    
  1. 2 class PublishReportingService
  2. 2 def initialize(recruitment_cycle_scope: RecruitmentCycle)
  3. 11 @courses = recruitment_cycle_scope.courses
  4. 11 @providers = recruitment_cycle_scope.providers
  5. end
  6. 2 class << self
  7. 2 def call(recruitment_cycle_scope:)
  8. 11 new(recruitment_cycle_scope: recruitment_cycle_scope).call
  9. end
  10. end
  11. 2 def call
  12. {
  13. 11 users: user_breakdown,
  14. providers: provider_breakdown,
  15. courses: course_breakdown,
  16. }
  17. end
  18. 2 private_class_method :new
  19. 2 private
  20. 2 def days_ago
  21. 33 @days_ago ||= 30.days.ago
  22. end
  23. 2 def active_users
  24. 22 @active_users ||= User.active
  25. end
  26. 2 def recent_active_users
  27. 33 @recent_active_users ||= active_users.last_login_since(days_ago)
  28. end
  29. 2 def providers_with_recent_active_users_distinct_count
  30. 22 @providers_with_recent_active_users_distinct_count ||= @providers
  31. .joins(:users)
  32. .merge(recent_active_users)
  33. .distinct
  34. .count
  35. end
  36. 2 def recent_active_user_count_by_provider
  37. 11 @recent_active_user_count_by_provider ||= recent_active_users
  38. .joins(:providers) # Results include a user entry for _each_ matching provider
  39. .merge(@providers) # Limit our scope to the current recruitment Cycle
  40. .group("provider_id")
  41. .count # Count the users for each provider
  42. end
  43. 2 def grouped_providers_with_x_active_users
  44. 56 @grouped_providers_with_x_active_users ||= recent_active_user_count_by_provider
  45. .group_by(&:second) # Group the results by the number of users they have
  46. .transform_values(&:count) # Count the results
  47. end
  48. 2 def with_more_than_5_recent_active_users
  49. 12 grouped_providers_with_x_active_users.keys.excluding(4, 3, 2, 1).sum { |k| grouped_providers_with_x_active_users[k] || 0 }
  50. end
  51. 2 def user_count
  52. 22 @user_count ||= User.count
  53. end
  54. 2 def active_users_count
  55. 22 @active_users_count ||= active_users.count
  56. end
  57. 2 def recent_active_users_count
  58. 11 @recent_active_users_count ||= recent_active_users.count
  59. end
  60. 2 def provider_count
  61. 22 @provider_count ||= @providers.count
  62. end
  63. 2 def user_breakdown
  64. {
  65. 11 total: {
  66. all: user_count,
  67. active_users: active_users_count,
  68. non_active_users: user_count - active_users_count,
  69. },
  70. recent_active_users: recent_active_users_count,
  71. }
  72. end
  73. 2 def provider_breakdown
  74. {
  75. 11 total: {
  76. all: provider_count,
  77. 11 providers_with_non_active_users: (provider_count - providers_with_recent_active_users_distinct_count),
  78. providers_with_recent_active_users: providers_with_recent_active_users_distinct_count,
  79. },
  80. with_1_recent_active_users: grouped_providers_with_x_active_users[1] || 0,
  81. with_2_recent_active_users: grouped_providers_with_x_active_users[2] || 0,
  82. with_3_recent_active_users: grouped_providers_with_x_active_users[3] || 0,
  83. with_4_recent_active_users: grouped_providers_with_x_active_users[4] || 0,
  84. with_more_than_5_recent_active_users: with_more_than_5_recent_active_users,
  85. }
  86. end
  87. 2 def course_breakdown
  88. 11 courses_changed_recently = @courses.changed_at_since(days_ago)
  89. 11 findable_courses = courses_changed_recently.findable.distinct
  90. 11 open_courses = findable_courses.with_vacancies
  91. 11 closed_courses = findable_courses.where.not(id: open_courses)
  92. 11 courses_changed_recently_count = courses_changed_recently.count
  93. 11 findable_courses_count = findable_courses.count
  94. {
  95. 11 total_updated_recently: courses_changed_recently_count,
  96. updated_non_findable_recently: courses_changed_recently_count - findable_courses_count,
  97. updated_findable_recently: findable_courses_count,
  98. updated_open_courses_recently: open_courses.count,
  99. updated_closed_courses_recently: closed_courses.count,
  100. created_recently: @courses.created_at_since(days_ago).count,
  101. }
  102. end
  103. end

app/services/record_first_login_service.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 2 class RecordFirstLoginService
  2. 2 def execute(current_user:)
  3. 3 return unless current_user.first_login_date_utc.nil?
  4. 1 current_user.update(first_login_date_utc: Time.now.utc)
  5. end
  6. end

app/services/recruitment_cycle_creation_service.rb

90.91% lines covered

11 relevant lines. 10 lines covered and 1 lines missed.
    
  1. 1 class RecruitmentCycleCreationService
  2. 1 include ServicePattern
  3. 1 def initialize(year:, application_start_date:, application_end_date:, summary: false)
  4. 1 @year = year
  5. 1 @application_start_date = application_start_date
  6. 1 @application_end_date = application_end_date
  7. 1 @summary = summary
  8. end
  9. 1 def call
  10. 1 RecruitmentCycle.create!(year: @year, application_start_date: @application_start_date, application_end_date: @application_end_date)
  11. 1 if @summary
  12. Rails.logger.info { "The new RecruitmentCycle has been successfully created for:\n\nyear: '#{@year}'\napplication_start_date: '#{@application_start_date}'\napplication_end_date: '#{@application_end_date}'\n\n" }
  13. end
  14. end
  15. end

app/services/rollover_reporting_service.rb

100.0% lines covered

31 relevant lines. 31 lines covered and 0 lines missed.
    
  1. 2 class RolloverReportingService
  2. 2 def initialize
  3. 16 @rollover_scope = RecruitmentCycle.current_recruitment_cycle
  4. end
  5. 2 class << self
  6. 2 delegate :call, to: :new
  7. end
  8. 2 def call
  9. 16 if @rollover_scope.next.blank?
  10. 11 { total: {
  11. published_courses: 0,
  12. new_courses_published: 0,
  13. deleted_courses: 0,
  14. existing_courses_in_draft: 0,
  15. existing_courses_in_review: 0,
  16. } }
  17. else
  18. {
  19. 5 total: {
  20. published_courses: published_courses_count,
  21. new_courses_published: new_courses_published_count,
  22. deleted_courses: deleted_courses_count,
  23. existing_courses_in_draft: existing_courses_in_draft_count,
  24. existing_courses_in_review: existing_courses_in_review,
  25. },
  26. }
  27. end
  28. end
  29. 2 private_class_method :new
  30. 2 private
  31. 2 def next_findable
  32. 25 @rollover_scope.next.courses.findable
  33. end
  34. 2 def published_courses
  35. 20 next_findable
  36. end
  37. 2 def published_courses_count
  38. 5 published_courses.distinct.count
  39. end
  40. 2 def new_courses_published
  41. 5 next_findable.distinct.created_at_since(RecruitmentCycle.next.created_at + 1.day)
  42. end
  43. 2 def new_courses_published_count
  44. 5 new_courses_published.count
  45. end
  46. 2 def deleted_courses
  47. 10 Course.discarded.where(provider: Provider.where(recruitment_cycle: RecruitmentCycle.next))
  48. end
  49. 2 def deleted_courses_count
  50. 5 deleted_courses.count
  51. end
  52. 2 def existing_courses_in_draft
  53. 10 @rollover_scope.next.courses.changed_since(RecruitmentCycle.next.created_at + 1.day).where.not(id: published_courses)
  54. end
  55. 2 def existing_courses_in_draft_count
  56. 5 existing_courses_in_draft.distinct.count
  57. end
  58. 2 def existing_courses_in_review
  59. 5 @rollover_scope.next.courses.where.not(id: published_courses).where.not(id: deleted_courses).where.not(id: existing_courses_in_draft).count
  60. end
  61. end

app/services/rollover_service.rb

100.0% lines covered

28 relevant lines. 28 lines covered and 0 lines missed.
    
  1. 1 class RolloverService
  2. 1 include ServicePattern
  3. 1 def initialize(provider_codes:, force: false)
  4. 6 @provider_codes = provider_codes
  5. 6 @force = force
  6. end
  7. 1 def call
  8. 6 total_counts = { providers: 0, sites: 0, courses: 0 }
  9. 6 total_bm = Benchmark.measure do
  10. 15 providers.each { |provider| rollover(provider, total_counts) }
  11. end
  12. 6 Rails.logger.info "Rollover done: " \
  13. "#{total_counts[:providers]} providers, " \
  14. "#{total_counts[:sites]} sites, " \
  15. "#{total_counts[:courses]} courses " +
  16. format("in %.3f seconds", total_bm.real)
  17. end
  18. 1 private
  19. 1 attr_reader :provider_codes, :force
  20. 1 def rollover(provider, total_counts)
  21. 9 Rails.logger.info { "Copying provider #{provider.provider_code}: " }
  22. 9 counts = nil
  23. 9 bm = Benchmark.measure do
  24. 9 Provider.connection.transaction do
  25. 9 copy_courses_to_provider_service = Courses::CopyToProviderService.new(
  26. sites_copy_to_course: Sites::CopyToCourseService.new,
  27. enrichments_copy_to_course: Enrichments::CopyToCourseService.new,
  28. )
  29. 9 copy_provider_to_recruitment_cycle = Providers::CopyToRecruitmentCycleService.new(
  30. copy_course_to_provider_service: copy_courses_to_provider_service,
  31. copy_site_to_provider_service: Sites::CopyToProviderService.new,
  32. logger: Logger.new($stdout),
  33. )
  34. 9 counts = copy_provider_to_recruitment_cycle.execute(
  35. provider: provider, new_recruitment_cycle: new_recruitment_cycle, force: force,
  36. )
  37. end
  38. end
  39. 9 Rails.logger.info "provider #{counts[:providers].zero? ? 'skipped' : 'copied'}, " \
  40. "#{counts[:sites]} sites copied, " \
  41. "#{counts[:courses]} courses copied " +
  42. format("in %.3f seconds", bm.real)
  43. 36 total_counts.merge!(counts) { |_, total, count| total + count }
  44. end
  45. 1 def new_recruitment_cycle
  46. 9 @new_recruitment_cycle ||= RecruitmentCycle.next_recruitment_cycle
  47. end
  48. 1 def providers
  49. 6 @providers ||= if provider_codes.any?
  50. 3 RecruitmentCycle.current_recruitment_cycle
  51. .providers.where(provider_code: provider_codes.to_a.map(&:upcase))
  52. else
  53. 3 RecruitmentCycle.current_recruitment_cycle.providers
  54. end
  55. end
  56. end

app/services/send_welcome_email_service.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 class SendWelcomeEmailService
  2. 1 class MissingFirstNameError < StandardError; end
  3. 1 class << self
  4. 1 def call(current_user:)
  5. 18 new.call(current_user: current_user)
  6. end
  7. end
  8. 1 def call(current_user:)
  9. 18 return if current_user.welcome_email_date_utc
  10. # Getting visibility on the missing personalisation first_name issue
  11. 14 raise MissingFirstNameError, "This user (user.id: #{current_user.id}) does not have a first name." if current_user.first_name.blank?
  12. 8 WelcomeEmailMailer
  13. .send_welcome_email(first_name: current_user.first_name, email: current_user.email)
  14. .deliver_later
  15. 8 current_user.update(
  16. welcome_email_date_utc: Time.now.utc,
  17. )
  18. rescue MissingFirstNameError => e
  19. 6 Sentry.capture_exception(e)
  20. end
  21. end

app/services/serialize_course_service.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 class SerializeCourseService
  2. 1 def initialize(serializers_service: CourseSerializersService.new, renderer: JSONAPI::Serializable::Renderer.new)
  3. 3 @serializers_service = serializers_service
  4. 3 @renderer = renderer
  5. end
  6. 1 def execute(object:)
  7. 3 { serialized: @renderer.render(object, class: @serializers_service.execute) }
  8. end
  9. end

app/services/service_container.rb

77.27% lines covered

22 relevant lines. 17 lines covered and 5 lines missed.
    
  1. 1 require "dry/container"
  2. 1 class ServiceContainer
  3. 1 def initialize
  4. 1 @services = Dry::Container.new
  5. 1 register_services
  6. end
  7. 1 def define(namespace, name, &block)
  8. 6 @services.namespace(namespace) do
  9. 6 register(name, block)
  10. end
  11. end
  12. 1 def get(namespace, service_name)
  13. 1 @services.resolve("#{namespace}.#{service_name}")
  14. end
  15. 1 private
  16. 1 def register_services
  17. 1 define(:courses, :copy_to_provider) do
  18. Courses::CopyToProviderService.new(
  19. sites_copy_to_course: get(:sites, :copy_to_course),
  20. enrichments_copy_to_course: get(:course_enrichments, :copy_to_course),
  21. )
  22. end
  23. 1 define(:sites, :copy_to_course) do
  24. Sites::CopyToCourseService.new
  25. end
  26. 1 define(:sites, :copy_to_provider) do
  27. Sites::CopyToProviderService.new
  28. end
  29. 1 define(:course_enrichments, :copy_to_course) do
  30. Enrichments::CopyToCourseService.new
  31. end
  32. 1 define(:providers, :copy_to_recruitment_cycle) do
  33. Providers::CopyToRecruitmentCycleService.new(
  34. copy_course_to_provider_service: get(:courses, :copy_to_provider),
  35. copy_site_to_provider_service: get(:sites, :copy_to_provider),
  36. )
  37. end
  38. end
  39. end

app/services/service_pattern.rb

90.91% lines covered

11 relevant lines. 10 lines covered and 1 lines missed.
    
  1. 4 module ServicePattern
  2. 4 def self.included(base)
  3. 42 base.extend ClassMethods
  4. 42 base.class_eval do
  5. 42 private_class_method :new
  6. end
  7. end
  8. 4 def call
  9. raise NotImplementedError("#call must be implemented")
  10. end
  11. 4 module ClassMethods
  12. 4 def call(**args)
  13. 753 return new.call if args.empty?
  14. 690 new(args).call
  15. end
  16. end
  17. end

app/services/sites/code_generator.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. 3 module Sites
  2. 3 class CodeGenerator
  3. 3 include ServicePattern
  4. 3 def initialize(provider:)
  5. 20 @provider = provider
  6. end
  7. 3 def call
  8. 20 ucas_style_code.presence || highest_site_code.next
  9. end
  10. 3 private
  11. 3 attr_reader :provider
  12. 3 def highest_site_code
  13. 5 return "Z" if existing_sequential_codes.blank?
  14. 3 existing_sequential_codes.max
  15. end
  16. 3 def existing_sequential_codes
  17. 8 (existing_site_codes - Site::POSSIBLE_CODES).compact
  18. end
  19. 3 def existing_site_codes
  20. 48 @existing_site_codes ||= provider.sites.pluck(:code)
  21. end
  22. 3 def unassigned_ucas_style_site_codes
  23. 40 Site::POSSIBLE_CODES - existing_site_codes
  24. end
  25. 3 def ucas_style_code
  26. 20 @ucas_style_code ||= begin
  27. 20 available_desirable_codes = unassigned_ucas_style_site_codes & Site::DESIRABLE_CODES
  28. 20 available_undesirable_codes = unassigned_ucas_style_site_codes & Site::EASILY_CONFUSED_CODES
  29. 20 available_desirable_codes.sample || available_undesirable_codes.sample
  30. end
  31. end
  32. end
  33. end

app/services/sites/copy_to_course_service.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 2 module Sites
  2. 2 class CopyToCourseService
  3. 2 def execute(new_site:, new_course:)
  4. 4 new_vac_status = SiteStatus.default_vac_status_given(
  5. study_mode: new_course.study_mode,
  6. )
  7. 4 new_course.site_statuses.create(
  8. site: new_site,
  9. vac_status: new_vac_status,
  10. status: :new_status,
  11. )
  12. end
  13. end
  14. end

app/services/sites/copy_to_provider_service.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 2 module Sites
  2. 2 class CopyToProviderService
  3. 2 def execute(site:, new_provider:)
  4. 4 new_site = new_provider.sites.find_by(code: site.code)
  5. 4 return if new_site.present?
  6. 3 new_site = site.dup
  7. 3 new_site.provider_id = new_provider.id
  8. 3 new_site.skip_geocoding = true
  9. 3 new_site.uuid = SecureRandom.uuid
  10. 3 new_site.save(validate: false)
  11. 3 new_provider.reload
  12. 3 new_site
  13. end
  14. end
  15. end

app/services/statistic_service.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 3 class StatisticService
  2. 3 def self.reporting(recruitment_cycle:)
  3. {
  4. 12 providers: ProviderReportingService.call(providers_scope: recruitment_cycle.providers),
  5. courses: CourseReportingService.call(courses_scope: recruitment_cycle.courses),
  6. publish: PublishReportingService.call(recruitment_cycle_scope: recruitment_cycle),
  7. allocations: AllocationReportingService.call(recruitment_cycle_scope: RecruitmentCycle.find_by(year: Settings.allocation_cycle_year)),
  8. rollover: RolloverReportingService.call,
  9. }
  10. end
  11. 3 def self.save(recruitment_cycle: RecruitmentCycle.current_recruitment_cycle)
  12. 6 Statistic.create!(json_data: reporting(recruitment_cycle: recruitment_cycle))
  13. end
  14. end

app/services/subjects/creator_service.rb

100.0% lines covered

24 relevant lines. 24 lines covered and 0 lines missed.
    
  1. 4 module Subjects
  2. 4 class CreatorService
  3. 4 def initialize(primary_subject: ::PrimarySubject,
  4. secondary_subject: ::SecondarySubject,
  5. further_education_subject: ::FurtherEducationSubject,
  6. modern_languages_subject: ::ModernLanguagesSubject,
  7. discontinued_subject: ::DiscontinuedSubject)
  8. 316 @primary_subject = primary_subject
  9. 316 @secondary_subject = secondary_subject
  10. 316 @further_education_subject = further_education_subject
  11. 316 @modern_languages_subject = modern_languages_subject
  12. 316 @discontinued_subject = discontinued_subject
  13. end
  14. 4 def execute
  15. primary = [
  16. 316 { subject_name: "Primary", subject_code: "00" },
  17. { subject_name: "Primary with English", subject_code: "01" },
  18. { subject_name: "Primary with geography and history", subject_code: "02" },
  19. { subject_name: "Primary with mathematics", subject_code: "03" },
  20. { subject_name: "Primary with modern languages", subject_code: "04" },
  21. { subject_name: "Primary with physical education", subject_code: "06" },
  22. { subject_name: "Primary with science", subject_code: "07" },
  23. ]
  24. secondary = [
  25. 316 { subject_name: "Art and design", subject_code: "W1" },
  26. { subject_name: "Science", subject_code: "F0" },
  27. { subject_name: "Biology", subject_code: "C1" },
  28. { subject_name: "Business studies", subject_code: "08" },
  29. { subject_name: "Chemistry", subject_code: "F1" },
  30. { subject_name: "Citizenship", subject_code: "09" },
  31. { subject_name: "Classics", subject_code: "Q8" },
  32. { subject_name: "Communication and media studies", subject_code: "P3" },
  33. { subject_name: "Computing", subject_code: "11" },
  34. { subject_name: "Dance", subject_code: "12" },
  35. { subject_name: "Design and technology", subject_code: "DT" },
  36. { subject_name: "Drama", subject_code: "13" },
  37. { subject_name: "Economics", subject_code: "L1" },
  38. { subject_name: "English", subject_code: "Q3" },
  39. { subject_name: "Geography", subject_code: "F8" },
  40. { subject_name: "Health and social care", subject_code: "L5" },
  41. { subject_name: "History", subject_code: "V1" },
  42. { subject_name: "Mathematics", subject_code: "G1" },
  43. { subject_name: "Music", subject_code: "W3" },
  44. { subject_name: "Philosophy", subject_code: "P1" },
  45. { subject_name: "Physical education", subject_code: "C6" },
  46. { subject_name: "Physics", subject_code: "F3" },
  47. { subject_name: "Psychology", subject_code: "C8" },
  48. { subject_name: "Religious education", subject_code: "V6" },
  49. { subject_name: "Social sciences", subject_code: "14" },
  50. # NOTE: no subject_code for 'Modern Languages' because this is just a stub used to trigger
  51. # selection of actual entries from `modern_languages` list
  52. { subject_name: "Modern Languages", subject_code: nil },
  53. # NOTE: These subject_code are contrived.
  54. { subject_name: "Latin", subject_code: "A0" },
  55. { subject_name: "Ancient Greek", subject_code: "A1" },
  56. { subject_name: "Ancient Hebrew", subject_code: "A2" },
  57. ]
  58. modern_languages = [
  59. 316 { subject_name: "French", subject_code: "15" },
  60. { subject_name: "English as a second or other language", subject_code: "16" },
  61. { subject_name: "German", subject_code: "17" },
  62. { subject_name: "Italian", subject_code: "18" },
  63. { subject_name: "Japanese", subject_code: "19" },
  64. { subject_name: "Mandarin", subject_code: "20" },
  65. { subject_name: "Russian", subject_code: "21" },
  66. { subject_name: "Spanish", subject_code: "22" },
  67. { subject_name: "Modern languages (other)", subject_code: "24" },
  68. ]
  69. further_education = [
  70. 316 { subject_name: "Further education", subject_code: "41" },
  71. ]
  72. # old 2019 DfE subjects
  73. discontinued = [
  74. 316 { subject_name: "Humanities" },
  75. { subject_name: "Balanced Science" },
  76. ]
  77. 316 primary.each do |subject|
  78. 2212 @primary_subject.find_or_create_by!(subject_name: subject[:subject_name], subject_code: subject[:subject_code])
  79. end
  80. 316 secondary.each do |subject|
  81. 9164 @secondary_subject.find_or_create_by!(subject_name: subject[:subject_name], subject_code: subject[:subject_code])
  82. end
  83. 316 modern_languages.each do |subject|
  84. 2844 @modern_languages_subject.find_or_create_by!(subject_name: subject[:subject_name], subject_code: subject[:subject_code])
  85. end
  86. 316 further_education.each do |subject|
  87. 316 @further_education_subject.find_or_create_by!(subject_name: subject[:subject_name], subject_code: subject[:subject_code])
  88. end
  89. 316 discontinued.each do |subject|
  90. 632 @discontinued_subject.find_or_create_by!(subject_name: subject[:subject_name])
  91. end
  92. end
  93. end
  94. end

app/services/subjects/financial_incentive_creator_service.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 4 module Subjects
  2. 4 class FinancialIncentiveCreatorService
  3. 4 def initialize(year:, subject: Subject, financial_incentive: FinancialIncentive)
  4. 317 @subject = subject
  5. 317 @financial_incentive = financial_incentive
  6. 317 @year = year
  7. end
  8. 4 def subject_and_financial_incentives
  9. subject_and_financial_incentives = {
  10. 317 2022 => {
  11. ["Latin", "Ancient Greek", "Ancient Hebrew"] => {
  12. bursary_amount: "15000",
  13. },
  14. },
  15. 2021 => {
  16. %w[Biology] => {
  17. bursary_amount: "7000",
  18. },
  19. %w[Chemistry Computing Mathematics Physics] => {
  20. scholarship: "26000",
  21. bursary_amount: "24000",
  22. },
  23. %w[Classics] => {
  24. bursary_amount: "10000",
  25. },
  26. ["French", "German", "Italian", "Japanese", "Mandarin", "Modern languages (other)", "Russian", "Spanish"] => { bursary_amount: "10000" },
  27. },
  28. 2020 => {
  29. ["Primary with mathematics"] => {
  30. bursary_amount: "6000",
  31. },
  32. %w[Biology Classics] => {
  33. bursary_amount: "26000",
  34. },
  35. %w[French German Spanish] => {
  36. bursary_amount: "26000",
  37. scholarship: "28000",
  38. early_career_payments: "2000",
  39. },
  40. %w[Computing] => {
  41. bursary_amount: "26000",
  42. scholarship: "28000",
  43. },
  44. %w[Geography] => {
  45. bursary_amount: "15000",
  46. scholarship: "17000",
  47. },
  48. ["Italian", "Japanese", "Mandarin", "Russian", "Modern languages (other)"] => {
  49. bursary_amount: "26000",
  50. early_career_payments: "2000",
  51. },
  52. ["Art and design", "Business studies", "History", "Music", "Religious education"] => {
  53. bursary_amount: "9000",
  54. },
  55. %w[English] => {
  56. bursary_amount: "12000",
  57. },
  58. ["Design and technology"] => {
  59. bursary_amount: "15000",
  60. },
  61. %w[Chemistry Mathematics Physics] => {
  62. bursary_amount: "26000",
  63. scholarship: "28000",
  64. early_career_payments: "2000",
  65. },
  66. },
  67. }
  68. 317 subject_and_financial_incentives[@year] || {}
  69. end
  70. 4 def execute
  71. 317 subject_and_financial_incentives.each do |subject_name, financial_incentive_attributes|
  72. 1271 @subject.where(subject_name: subject_name).each do |subject|
  73. 4443 @financial_incentive.find_or_create_by(subject: subject, **financial_incentive_attributes)
  74. end
  75. end
  76. end
  77. end
  78. end

app/services/subjects/financial_incentive_set_subject_knowledge_enhancement_course_available_service.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 4 module Subjects
  2. 4 class FinancialIncentiveSetSubjectKnowledgeEnhancementCourseAvailableService
  3. 4 def initialize(year:, financial_incentive: FinancialIncentive)
  4. 316 @financial_incentive = financial_incentive
  5. 316 @year = year
  6. end
  7. 4 def subject_with_subject_knowledge_enhancement_course_available
  8. subject_with_subject_knowledge_enhancement_course_available = {
  9. 316 2020 => ["Primary with mathematics", "Biology", "Computing", "English", "Design and technology", "Geography", "Chemistry", "Mathematics", "Physics", "French", "German", "Spanish", "Italian", "Japanese", "Mandarin", "Russian", "Modern languages (other)", "Religious education"],
  10. }
  11. 316 subject_with_subject_knowledge_enhancement_course_available[@year]
  12. end
  13. 4 def execute
  14. 316 financial_incentives = @financial_incentive.joins(:subject).where(subject: { subject_name: subject_with_subject_knowledge_enhancement_course_available })
  15. 316 financial_incentives.update_all(subject_knowledge_enhancement_course_available: true)
  16. end
  17. end
  18. end

app/services/subjects/subject_area_creator_service.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 4 module Subjects
  2. 4 class SubjectAreaCreatorService
  3. 4 def initialize(subject_area: SubjectArea)
  4. 316 @subject_area = subject_area
  5. end
  6. 4 def execute
  7. 316 @subject_area.find_or_create_by!(typename: "PrimarySubject", name: "Primary")
  8. 316 @subject_area.find_or_create_by!(typename: "SecondarySubject", name: "Secondary")
  9. 316 @subject_area.find_or_create_by!(typename: "ModernLanguagesSubject", name: "Secondary: Modern languages")
  10. 316 @subject_area.find_or_create_by!(typename: "FurtherEducationSubject", name: "Further education")
  11. 316 @subject_area.find_or_create_by!(typename: "DiscontinuedSubject", name: "Discontinued")
  12. end
  13. end
  14. end

app/services/support/data_exports/base.rb

91.67% lines covered

12 relevant lines. 11 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Support
  3. 1 module DataExports
  4. 1 class Base
  5. 1 def to_csv(data_for_export: data)
  6. 1 require "csv"
  7. 1 header_row = data_for_export.first.keys
  8. 1 ::CSV.generate(headers: true) do |rows|
  9. 1 rows << header_row
  10. 1 data_for_export.map(&:values).each do |value|
  11. 2 rows << value
  12. end
  13. end
  14. end
  15. 1 def filename
  16. "#{self.class.name.parameterize}_#{Time.zone.now.strftime('%Y-%m-%d_%H-%M_%S')}.csv"
  17. end
  18. end
  19. end
  20. end

app/services/support/data_exports/data_export.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Support
  3. 1 module DataExports
  4. 1 class DataExport
  5. 1 class << self
  6. 1 def all
  7. [
  8. 3 DataExports::UsersExport,
  9. ].map(&:new)
  10. end
  11. 1 def find(type)
  12. 2 all.detect { |d| d.type == type }
  13. end
  14. end
  15. end
  16. end
  17. end

app/services/support/data_exports/users_export.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Support
  3. 1 module DataExports
  4. 1 class UsersExport < Base
  5. 1 def type
  6. 3 "users"
  7. end
  8. 1 def data
  9. 1 result = []
  10. 1 RecruitmentCycle.current.providers.find_each do |provider|
  11. 1 provider.users.find_each do |user|
  12. 2 result << user_data(user, provider)
  13. end
  14. end
  15. 1 result
  16. end
  17. 1 private
  18. 1 def user_data(user, provider)
  19. {
  20. 3 user_id: user.id,
  21. provider_code: provider.provider_code,
  22. provider_name: provider.provider_name,
  23. provider_type: provider.provider_type,
  24. first_name: user.first_name,
  25. last_name: user.last_name,
  26. email_address: user.email,
  27. first_login_at: user.first_login_date_utc,
  28. last_login_at: user.last_login_date_utc,
  29. sign_in_user_id: user.sign_in_user_id,
  30. }
  31. end
  32. end
  33. end
  34. end

app/services/support/filter.rb

96.88% lines covered

32 relevant lines. 31 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module Support
  3. 4 class Filter
  4. 4 include ServicePattern
  5. 4 def initialize(model_data_scope:, filter_params:)
  6. 50 @model_data_scope = model_data_scope
  7. 50 @filter_params = filter_params
  8. end
  9. 4 def call
  10. 50 filter_records.page(filter_params[:page] || 1)
  11. end
  12. 4 private
  13. 4 attr_reader :model_data_scope, :filter_params
  14. 4 def text_search(records, text_search)
  15. 23 return records if text_search.blank?
  16. 7 records.search(text_search)
  17. end
  18. 4 def course_search(records, search)
  19. 27 return records if search.blank?
  20. 7 records.course_search(search)
  21. end
  22. 4 def provider_search(records, search)
  23. 27 return records if search.blank?
  24. 9 records.provider_search(search)
  25. end
  26. 4 def user_type(records, user_type_arr)
  27. 16 return records if user_type_arr&.all?(&:blank?)
  28. 16 case user_type_arr
  29. when ["admin"]
  30. records.admins
  31. when ["provider"]
  32. 2 records.non_admins
  33. else
  34. 14 records
  35. end
  36. end
  37. 4 def filter_records
  38. 50 filtered_records = model_data_scope
  39. 50 filtered_records = text_search(filtered_records, filter_params[:text_search]) if filtered_records.respond_to?(:search)
  40. 50 filtered_records = provider_search(filtered_records, filter_params[:provider_search]) if filtered_records.respond_to?(:provider_search)
  41. 50 filtered_records = course_search(filtered_records, filter_params[:course_search]) if filtered_records.respond_to?(:course_search)
  42. 50 filtered_records = user_type(filtered_records, filter_params[:user_type]) if filtered_records.respond_to?(:admins)
  43. 50 filtered_records
  44. end
  45. end
  46. end

app/services/token/decode_service.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 2 module Token
  2. 2 class DecodeService
  3. 2 include ServicePattern
  4. 2 def initialize(encoded_token:,
  5. secret:,
  6. algorithm:,
  7. audience:,
  8. issuer:,
  9. subject:)
  10. 30 @encoded_token = encoded_token
  11. 30 @secret = secret
  12. 30 @algorithm = algorithm
  13. 30 @audience = audience
  14. 30 @issuer = issuer
  15. 30 @subject = subject
  16. end
  17. 2 def call
  18. 30 decoded_token = JWT.decode(
  19. encoded_token,
  20. secret,
  21. true,
  22. {
  23. algorithm: algorithm,
  24. verify_iss: true,
  25. verify_aud: true,
  26. verify_sub: true,
  27. verify_iat: true,
  28. exp_leeway: 6.seconds.to_i,
  29. **claims,
  30. },
  31. )
  32. 24 (payload, _algorithm) = decoded_token
  33. 24 payload.with_indifferent_access[:data]
  34. end
  35. 2 private
  36. 2 attr_reader :encoded_token, :secret, :algorithm, :audience, :issuer, :subject
  37. 2 def claims
  38. 30 @claims ||= {
  39. aud: audience,
  40. iss: issuer,
  41. sub: subject,
  42. }
  43. end
  44. end
  45. end

app/services/user_associations_service/create.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. 1 module UserAssociationsService
  2. 1 class Create
  3. 1 attr_reader :provider, :user, :all_providers
  4. 1 class << self
  5. 1 def call(**args)
  6. 8 new(args).call
  7. end
  8. end
  9. 1 def initialize(user:, provider: nil, all_providers: false)
  10. 8 @provider = provider
  11. 8 @user = user
  12. 8 @all_providers = all_providers
  13. end
  14. 1 def call
  15. 8 add_user_to_providers
  16. 8 update_user_notification_preferences
  17. end
  18. 1 private_class_method :new
  19. 1 private
  20. 1 def add_user_to_providers
  21. 8 if all_providers
  22. 4 add_user_to_all_providers
  23. else
  24. 4 add_user_to_a_single_provider
  25. end
  26. end
  27. 1 def add_user_to_a_single_provider
  28. 4 provider.users << user
  29. end
  30. 1 def add_user_to_all_providers
  31. 4 user.providers = Provider.all
  32. end
  33. 1 def update_user_notification_preferences
  34. 8 user_notification_preferences = UserNotificationPreferences.new(user_id: user.id)
  35. 8 return if user_notification_preferences.updated_at.nil?
  36. 4 user_notification_preferences.update(
  37. enable_notifications: user_notification_preferences.enabled,
  38. )
  39. end
  40. end
  41. end

app/services/user_associations_service/delete.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. 1 module UserAssociationsService
  2. 1 class Delete
  3. 1 attr_reader :user, :providers
  4. 1 class << self
  5. 1 def call(**args)
  6. 6 new(args).call
  7. end
  8. end
  9. 1 def initialize(user:, providers:)
  10. 6 @providers = providers
  11. 6 @user = user
  12. end
  13. 1 def call
  14. 6 remove_access
  15. 6 update_user_notification_preferences
  16. end
  17. 1 private_class_method :new
  18. 1 private
  19. 1 def remove_access
  20. 6 user.remove_access_to(providers)
  21. end
  22. 1 def update_user_notification_preferences
  23. 6 user_notification_preferences = UserNotificationPreferences.new(user_id: user.id)
  24. 6 return if user_notification_preferences.updated_at.nil?
  25. 4 user_notification_preferences.update(
  26. enable_notifications: user_notification_preferences.enabled,
  27. )
  28. end
  29. end
  30. end

app/services/user_sessions/update.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 module UserSessions
  3. 4 class Update
  4. 4 include ServicePattern
  5. 4 attr_reader :user, :successful
  6. 4 alias_method :successful?, :successful
  7. 4 def initialize(user:, user_session:)
  8. 227 @user = user
  9. attributes = {
  10. 227 last_login_date_utc: Time.zone.now,
  11. email: user_session.email,
  12. sign_in_user_id: user_session.sign_in_user_id,
  13. }
  14. 227 attributes[:first_name] = user_session.first_name if user_session.first_name.present?
  15. 227 attributes[:last_name] = user_session.last_name if user_session.last_name.present?
  16. 227 @user.assign_attributes(attributes)
  17. end
  18. 4 def call
  19. 231 @successful = user.valid? && user.save!
  20. 231 self
  21. end
  22. end
  23. end

app/services/v3/course_search_service.rb

100.0% lines covered

112 relevant lines. 112 lines covered and 0 lines missed.
    
  1. 3 module V3
  2. 3 class CourseSearchService
  3. 3 def initialize(filter: nil, sort: nil, course_scope: Course)
  4. 96 @filter = filter || {}
  5. 96 @sort = Set.new(sort&.split(","))
  6. 96 @course_scope = course_scope
  7. end
  8. 3 class << self
  9. 3 def call(**args)
  10. 96 new(args).call
  11. end
  12. end
  13. 3 def call
  14. 96 scope = course_scope
  15. 96 scope = scope.with_salary if funding_filter_salary?
  16. 96 scope = scope.with_qualifications(qualifications) if qualifications.any?
  17. 96 scope = scope.with_vacancies if with_vacancies?
  18. 96 scope = scope.findable if findable?
  19. 96 scope = scope.with_study_modes(study_types) if study_types.any?
  20. 96 scope = scope.with_subjects(subject_codes) if subject_codes.any?
  21. 96 scope = scope.with_provider_name(provider_name) if provider_name.present?
  22. 96 scope = scope.with_send if send_courses_filter?
  23. 96 scope = scope.within(filter[:radius], origin: origin) if locations_filter?
  24. 96 scope = scope.with_funding_types(funding_types) if funding_types.any?
  25. 96 scope = scope.with_degree_grades(degree_grades) if degree_grades.any?
  26. 96 scope = scope.provider_can_sponsor_visa if can_sponsor_visa_filter?
  27. 96 scope = scope.includes(
  28. :enrichments,
  29. :financial_incentives,
  30. course_subjects: [:subject],
  31. site_statuses: [:site],
  32. provider: %i[recruitment_cycle ucas_preferences],
  33. )
  34. 96 if provider_name.present?
  35. 5 scope = scope
  36. .joins(:provider).order("by_provider_name")
  37. .ascending_canonical_order
  38. 5 scope = scope.select("DISTINCT(course.id), course.*, provider.provider_name, CASE WHEN provider.provider_name = #{ActiveRecord::Base.connection.quote(provider_name)} THEN '1' END as by_provider_name")
  39. 91 elsif sort_by_provider_ascending?
  40. 3 scope = scope.ascending_canonical_order
  41. 3 scope = scope.select("DISTINCT(course.id), course.*, provider.provider_name")
  42. 88 elsif sort_by_provider_descending?
  43. 2 scope = scope.descending_canonical_order
  44. 2 scope = scope.select("DISTINCT(course.id), course.*, provider.provider_name")
  45. 86 elsif sort_by_distance?
  46. 13 scope = scope.joins(courses_with_distance_from_origin)
  47. 13 scope = scope.joins(:provider)
  48. 13 scope = scope.select("DISTINCT(course.id), course.*, distance, #{Course.sanitize_sql(distance_with_university_area_adjustment)}")
  49. scope =
  50. 13 if expand_university?
  51. 1 scope.order(:boosted_distance)
  52. else
  53. 12 scope.order(:distance)
  54. end
  55. 73 elsif !scope.to_sql.include?("DISTINCT")
  56. 73 scope = scope.select("DISTINCT(course.id), course.*")
  57. end
  58. 96 scope
  59. end
  60. 3 private_class_method :new
  61. 3 private
  62. 3 def expand_university?
  63. 13 filter[:expand_university].to_s.downcase == "true"
  64. end
  65. 3 def distance_with_university_area_adjustment
  66. 13 university_provider_type = Provider.provider_types[:university]
  67. 13 university_location_area_radius = 10
  68. 13 <<~EOSQL.gsub(/\s+/m, " ").strip
  69. (CASE
  70. WHEN provider.provider_type = '#{university_provider_type}'
  71. THEN (distance - #{university_location_area_radius})
  72. ELSE distance
  73. END) as boosted_distance
  74. EOSQL
  75. end
  76. 3 def locatable_sites
  77. 26 site_statuses = SiteStatus.arel_table
  78. 26 sites = Site.arel_table
  79. # Create virtual table with sites and site statuses
  80. 26 site_statuses.join(sites).on(site_statuses[:site_id].eq(sites[:id]))
  81. .where(site_statuses_criteria(site_statuses))
  82. .where(already_geocoded_criteria(sites))
  83. .where(locatable_address_criteria(sites))
  84. end
  85. 3 def site_statuses_criteria(site_statuses)
  86. # Only running and published site statuses
  87. 26 running_and_published_criteria = site_statuses[:status].eq(SiteStatus.statuses[:running]).and(site_statuses[:publish].eq(SiteStatus.publishes[:published]))
  88. 26 if with_vacancies?
  89. # Only site statuses with vacancies
  90. 2 running_and_published_criteria
  91. .and(site_statuses[:vac_status])
  92. .eq_any([
  93. SiteStatus.vac_statuses[:full_time_vacancies],
  94. SiteStatus.vac_statuses[:part_time_vacancies],
  95. SiteStatus.vac_statuses[:both_full_time_and_part_time_vacancies],
  96. ])
  97. else
  98. 24 running_and_published_criteria
  99. end
  100. end
  101. 3 def already_geocoded_criteria(sites)
  102. # we only want sites that have been geocoded
  103. 26 sites[:latitude].not_eq(nil).and(sites[:longitude].not_eq(nil))
  104. end
  105. 3 def locatable_address_criteria(sites)
  106. # only sites that have a locatable address
  107. # there are some sites with no address1 or postcode that cannot be
  108. # accurately geocoded. We don't want to return these as the closest site.
  109. # This should be removed once the data is fixed
  110. 26 sites[:address1].not_eq("").or(sites[:postcode].not_eq(""))
  111. end
  112. 3 def course_id_with_lowest_locatable_distance
  113. # select course_id and nearest site with shortest distance from origin
  114. # as courses may have multiple sites
  115. # this will remove duplicates by aggregating on course_id
  116. 26 origin_lat_long = Struct.new(:lat, :lng).new(origin[0].to_f, origin[1].to_f)
  117. 26 lowest_locatable_distance = Arel.sql("MIN#{Site.sanitize_sql(Site.distance_sql(origin_lat_long))} as distance")
  118. 26 locatable_sites.project(:course_id, lowest_locatable_distance).group(:course_id)
  119. end
  120. 3 def distance_table
  121. # form a temporary table with results
  122. 26 Arel::Nodes::TableAlias.new(
  123. Arel.sql(
  124. format("(%s)", course_id_with_lowest_locatable_distance.to_sql),
  125. ), "distances"
  126. )
  127. end
  128. 3 def courses_with_distance_from_origin
  129. # grab courses table and join with the above result set
  130. # so distances from origin are now available
  131. # we can then sort by distance from the given origin
  132. 13 courses_table = Course.arel_table
  133. 13 courses_table.join(distance_table).on(courses_table[:id].eq(distance_table[:course_id])).join_sources
  134. end
  135. 3 def locations_filter?
  136. 96 filter.key?(:latitude) &&
  137. filter.key?(:longitude) &&
  138. filter.key?(:radius)
  139. end
  140. 3 def sort_by_provider_ascending?
  141. 91 sort == Set["name", "provider.provider_name"]
  142. end
  143. 3 def sort_by_provider_descending?
  144. 88 sort == Set["-name", "-provider.provider_name"]
  145. end
  146. 3 def sort_by_distance?
  147. 86 sort == Set["distance"]
  148. end
  149. 3 def origin
  150. 54 [filter[:latitude], filter[:longitude]]
  151. end
  152. 3 attr_reader :sort, :filter, :course_scope
  153. 3 def funding_filter_salary?
  154. 96 filter[:funding] == "salary"
  155. end
  156. 3 def qualifications
  157. 99 return [] if filter[:qualification].blank?
  158. 6 filter[:qualification].split(",")
  159. end
  160. 3 def with_vacancies?
  161. 122 filter[:has_vacancies].to_s.downcase == "true"
  162. end
  163. 3 def findable?
  164. 96 filter[:findable].to_s.downcase == "true"
  165. end
  166. 3 def study_types
  167. 102 return [] if filter[:study_type].blank?
  168. 12 filter[:study_type].split(",")
  169. end
  170. 3 def funding_types
  171. 100 return [] if filter[:funding_type].blank?
  172. 8 filter[:funding_type].split(",")
  173. end
  174. 3 def degree_grades
  175. 101 return [] if filter[:degree_grade].blank?
  176. 10 filter[:degree_grade].split(",")
  177. end
  178. 3 def subject_codes
  179. 102 return [] if filter[:subjects].blank?
  180. 12 filter[:subjects].split(",")
  181. end
  182. 3 def provider_name
  183. 202 return [] if filter[:"provider.provider_name"].blank?
  184. 20 filter[:"provider.provider_name"]
  185. end
  186. 3 def send_courses_filter?
  187. 96 filter[:send_courses].to_s.downcase == "true"
  188. end
  189. 3 def can_sponsor_visa_filter?
  190. 96 filter[:can_sponsor_visa].to_s.downcase == "true"
  191. end
  192. end
  193. end

app/services/vacancy_status_determination_service.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. 2 class VacancyStatusDeterminationService
  2. 2 attr_reader :vacancy_status_full_time,
  3. :vacancy_status_part_time,
  4. :course
  5. 2 def self.call(vacancy_status_full_time:, vacancy_status_part_time:, course:)
  6. 28 new(
  7. vacancy_status_full_time: vacancy_status_full_time,
  8. vacancy_status_part_time: vacancy_status_part_time,
  9. course: course,
  10. ).vacancy_status
  11. end
  12. 2 def initialize(vacancy_status_full_time:, vacancy_status_part_time:, course:)
  13. 28 @vacancy_status_full_time = vacancy_status_full_time
  14. 28 @vacancy_status_part_time = vacancy_status_part_time
  15. 28 @course = course
  16. end
  17. 2 def vacancy_status
  18. 28 return "both_full_time_and_part_time_vacancies" if full_or_part_time?
  19. 25 return "full_time_vacancies" if full_time?
  20. 15 return "part_time_vacancies" if part_time?
  21. 13 "no_vacancies"
  22. end
  23. 2 private
  24. 2 def part_time?
  25. 15 (course.full_time_or_part_time? && vacancy_status_part_time?) ||
  26. 14 (course.part_time? && vacancy_status_part_time?)
  27. end
  28. 2 def full_time?
  29. 25 (course.full_time_or_part_time? && vacancy_status_full_time?) ||
  30. 24 (course.full_time? && vacancy_status_full_time?)
  31. end
  32. 2 def full_or_part_time?
  33. 28 course.full_time_or_part_time? &&
  34. 6 (vacancy_status_full_time? && vacancy_status_part_time?)
  35. end
  36. 2 def vacancy_status_full_time?
  37. 29 vacancy_status_full_time == "1"
  38. end
  39. 2 def vacancy_status_part_time?
  40. 8 vacancy_status_part_time == "1"
  41. end
  42. end

app/validators/email_address_validator.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 4 class EmailAddressValidator < ActiveModel::EachValidator
  2. 4 EMAIL_VALIDATION_ERROR_MESSAGE = "^Enter an email address in the correct format, like name@example.com".freeze
  3. 4 def validate_each(record, attribute, value)
  4. 3456 unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
  5. 27 record.errors.add attribute, (options[:message] || EMAIL_VALIDATION_ERROR_MESSAGE)
  6. end
  7. end
  8. end

app/validators/phone_validator.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 4 class PhoneValidator < ActiveModel::EachValidator
  2. 4 PHONE_VALIDATION_ERROR_MESSAGE = "^Enter a valid telephone number".freeze
  3. 4 def validate_each(record, attribute, value)
  4. 3465 if value.blank? || is_invalid_phone_number_format?(value)
  5. 13 record.errors.add(attribute, message: options[:message] || PHONE_VALIDATION_ERROR_MESSAGE)
  6. end
  7. end
  8. 4 private
  9. 4 def is_invalid_phone_number_format?(value)
  10. 3456 value.match?(/[^ext()+\. 0-9]/)
  11. end
  12. end

app/validators/postcode_validator.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 4 class PostcodeValidator < ActiveModel::EachValidator
  2. 4 def validate_each(record, attribute, value)
  3. 833 return unless value
  4. 831 postcode = UKPostcode.parse(value)
  5. 831 unless postcode.full_valid?
  6. 3 record.errors.add(attribute, message: options[:message] || "is not valid (for example, BN1 1AA)")
  7. end
  8. end
  9. end

app/validators/reference_number_format_validator.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 4 class ReferenceNumberFormatValidator < ActiveModel::EachValidator
  2. 4 def validate_each(record, attribute, value)
  3. 7745 return if options[:allow_blank] && value.blank?
  4. 7745 if value.blank? || value.length > options[:maximum] || value.length < options[:minimum] || !value.match?(/\A-?\d+\Z/)
  5. 9 record.errors.add attribute, options[:message]
  6. end
  7. end
  8. end

app/validators/unique_course_validator.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 4 class UniqueCourseValidator < ActiveModel::Validator
  2. 4 def validate(record)
  3. 171 return if course_is_unique?(record)
  4. 3 record.errors.add(:base, :duplicate)
  5. end
  6. 4 private
  7. 4 def course_is_unique?(new_course)
  8. 171 existing_courses = new_course.provider.courses
  9. 171 new_course_attributes = new_course.attributes.slice(*attributes_to_compare)
  10. 171 existing_courses.none? do |existing_course|
  11. 28 existing_course_attributes = existing_course.attributes.slice(*attributes_to_compare)
  12. 28 new_course_attributes == existing_course_attributes &&
  13. new_course.subjects == existing_course.subjects &&
  14. new_course.accrediting_provider == existing_course.accrediting_provider
  15. end
  16. end
  17. 4 def attributes_to_compare
  18. 199 %w[level is_send age_range_in_years qualification program_type study_mode maths english science applications_open_from start_date]
  19. end
  20. end

app/validators/words_count_validator.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 4 class WordsCountValidator < ActiveModel::EachValidator
  2. 4 def validate_each(record, attribute, string)
  3. 14513 return if string.blank?
  4. 14216 if word_count(string) > options[:maximum]
  5. 37 record.errors.add(
  6. attribute,
  7. message: options[:message] || "^Reduce the word count for #{attribute.to_s.humanize(capitalize: false)}",
  8. )
  9. end
  10. end
  11. 4 def word_count(string)
  12. 14216 string.scan(/\S+/).size
  13. end
  14. end

app/view_objects/allocations_view.rb

100.0% lines covered

104 relevant lines. 104 lines covered and 0 lines missed.
    
  1. 3 class AllocationsView
  2. 3 include ActionView::Helpers::TextHelper
  3. 3 module Status
  4. 3 REQUESTED = "REQUESTED".freeze
  5. 3 NOT_REQUESTED = "NOT REQUESTED".freeze
  6. 3 YET_TO_REQUEST = "YET TO REQUEST".freeze
  7. 3 NO_REQUEST_SENT = "NO REQUEST SENT".freeze
  8. end
  9. 3 module Colour
  10. 3 GREEN = "green".freeze
  11. 3 RED = "red".freeze
  12. 3 GREY = "grey".freeze
  13. end
  14. 3 module Requested
  15. 3 YES = "yes".freeze
  16. 3 NO = "no".freeze
  17. end
  18. 3 module RequestType
  19. 3 INITIAL = "initial".freeze
  20. 3 REPEAT = "repeat".freeze
  21. 3 DECLINED = "declined".freeze
  22. end
  23. 3 def initialize(training_providers:, allocations:)
  24. 50 @training_providers = training_providers
  25. 50 @allocations = allocations
  26. end
  27. 3 def repeat_allocation_statuses
  28. 10 filtered_training_providers.map do |training_provider|
  29. 11 matching_allocation = find_matching_allocation(training_provider, repeat_allocations)
  30. 11 build_repeat_allocations(matching_allocation, training_provider)
  31. end
  32. end
  33. 3 def initial_allocation_statuses
  34. 36 statuses = initial_allocations.map do |allocation|
  35. 18 build_initial_allocations(allocation, allocation.provider)
  36. end
  37. 36 statuses.compact
  38. end
  39. 3 def requested_allocations_statuses
  40. 15 statuses = requested_allocations.map do |allocation|
  41. 10 build_requested_allocations(allocation, allocation.provider)
  42. end
  43. 25 statuses.compact.sort_by! { |hsh| hsh[:training_provider_name] }
  44. end
  45. 3 def confirmed_allocation_places
  46. 6 statuses = confirmed_allocations.map do |allocation|
  47. 4 build_confirmed_allocations(allocation, allocation.provider)
  48. end
  49. 10 statuses.compact.sort_by! { |hsh| hsh[:training_provider_name] }
  50. end
  51. 3 def not_requested_allocations_statuses
  52. 7 statuses = filtered_training_providers.map do |training_provider|
  53. 5 matching_allocation = find_matching_allocation(training_provider, not_requested_allocations)
  54. 5 build_not_requested_allocations(matching_allocation, training_provider)
  55. end
  56. 11 statuses.compact.sort_by! { |hsh| hsh[:training_provider_name] }
  57. end
  58. 3 private
  59. 3 def filtered_training_providers
  60. # When displaying 'repeat allocation statuses'
  61. # we need to first filter out those training providers
  62. # who will be allocated places for the first time (i.e. where the accredited provider)
  63. # has made initial allocation requests on their behalf)
  64. 21 training_provider_provider_codes = initial_allocations.map { |allocation| allocation.provider.provider_code }
  65. 36 @training_providers.reject { |tp| training_provider_provider_codes.include?(tp.provider_code) }
  66. end
  67. 3 def repeat_allocations
  68. 20 @allocations.reject { |allocation| allocation.request_type == RequestType::INITIAL }
  69. end
  70. 3 def initial_allocations
  71. 91 @allocations.select { |allocation| allocation.request_type == RequestType::INITIAL }
  72. end
  73. 3 def requested_allocations
  74. 27 @allocations.select { |allocation| allocation.request_type.in?([RequestType::INITIAL, RequestType::REPEAT]) }
  75. end
  76. 3 def confirmed_allocations
  77. 11 @allocations.select { |allocation| allocation.request_type.in?([RequestType::INITIAL, RequestType::REPEAT]) }
  78. end
  79. 3 def not_requested_allocations
  80. 7 @allocations.select { |allocation| allocation.request_type == RequestType::DECLINED }
  81. end
  82. 3 def find_matching_allocation(training_provider, allocations)
  83. 23 allocations.find { |allocation| allocation.provider.provider_code == training_provider.provider_code }
  84. end
  85. 3 def build_repeat_allocations(matching_allocation, training_provider)
  86. allocation_status = {
  87. 11 training_provider_name: training_provider.provider_name,
  88. training_provider_code: training_provider.provider_code,
  89. }
  90. 11 if yet_to_request?(matching_allocation)
  91. 5 allocation_status[:status] = Status::YET_TO_REQUEST
  92. 5 allocation_status[:status_colour] = Colour::GREY
  93. end
  94. 11 if requested?(matching_allocation)
  95. 4 allocation_status[:status] = Status::REQUESTED
  96. 4 allocation_status[:status_colour] = Colour::GREEN
  97. 4 allocation_status[:requested] = Requested::YES
  98. end
  99. 11 if not_requested?(matching_allocation)
  100. 2 allocation_status[:status] = Status::NOT_REQUESTED
  101. 2 allocation_status[:status_colour] = Colour::RED
  102. 2 allocation_status[:requested] = Requested::NO
  103. end
  104. 11 if matching_allocation
  105. 6 allocation_status[:id] = matching_allocation.id
  106. 6 allocation_status[:request_type] = matching_allocation.request_type
  107. end
  108. 11 if matching_allocation&.accredited_body
  109. 6 allocation_status[:accredited_body_code] = matching_allocation.accredited_body.provider_code
  110. end
  111. 11 allocation_status
  112. end
  113. 3 def build_initial_allocations(matching_allocation, training_provider)
  114. 18 return if matching_allocation.nil?
  115. hash = {
  116. 18 training_provider_name: training_provider.provider_name,
  117. training_provider_code: training_provider.provider_code,
  118. status_colour: Colour::GREEN,
  119. requested: Requested::YES,
  120. request_type: matching_allocation.request_type,
  121. status: "#{pluralize(matching_allocation.number_of_places, 'place')} requested".upcase,
  122. }
  123. 18 hash[:id] = matching_allocation.id if matching_allocation.id
  124. 18 hash
  125. end
  126. 3 def build_confirmed_allocations(allocation, training_provider)
  127. 4 return if allocation.nil?
  128. {
  129. 4 training_provider_name: training_provider.provider_name,
  130. number_of_places: allocation.number_of_places,
  131. confirmed_number_of_places: allocation.confirmed_number_of_places,
  132. uplifts: allocation.allocation_uplift&.uplifts,
  133. total: allocation.confirmed_number_of_places.to_i + allocation.allocation_uplift&.uplifts.to_i,
  134. }
  135. end
  136. 3 def build_requested_allocations(allocation, training_provider)
  137. 10 return if allocation.nil?
  138. {
  139. 10 training_provider_name: training_provider.provider_name,
  140. training_provider_code: training_provider.provider_code,
  141. status_colour: Colour::GREEN,
  142. status: Status::REQUESTED,
  143. }
  144. end
  145. 3 def build_not_requested_allocations(allocation, training_provider)
  146. 6 return if training_provider.provider_name.in?(requested_allocations_statuses.map { |tp| tp[:training_provider_name] })
  147. allocation_status = {
  148. 4 training_provider_name: training_provider.provider_name,
  149. training_provider_code: training_provider.provider_code,
  150. }
  151. 4 if yet_to_request?(allocation)
  152. 3 allocation_status[:status] = "NO REQUEST SENT"
  153. 3 allocation_status[:status_colour] = Colour::GREY
  154. end
  155. 4 if not_requested?(allocation)
  156. 1 allocation_status[:status] = Status::NOT_REQUESTED
  157. 1 allocation_status[:status_colour] = Colour::RED
  158. end
  159. 4 allocation_status
  160. end
  161. 3 def not_requested?(matching_allocation)
  162. 15 matching_allocation && matching_allocation[:request_type] == RequestType::DECLINED
  163. end
  164. 3 def requested?(matching_allocation)
  165. 11 matching_allocation && matching_allocation[:request_type] == RequestType::REPEAT
  166. end
  167. 3 def yet_to_request?(matching_allocation)
  168. 15 matching_allocation.nil?
  169. end
  170. end

app/view_objects/notifications_view.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 1 class NotificationsView
  2. 1 ORGANISATION_URL_PATTERN = /\/organisations\/(\S+)\/?/.freeze
  3. 1 def initialize(
  4. request:,
  5. current_user:
  6. )
  7. 9 @request = request
  8. 9 @current_user = current_user
  9. end
  10. 1 def user_id
  11. 1 current_user.id
  12. end
  13. 1 def user_email
  14. 1 current_user.email
  15. end
  16. 1 def back_link_path
  17. 5 return Rails.application.routes.url_helpers.root_path if ORGANISATION_URL_PATTERN.match(request.referer).nil?
  18. 3 URI(request.referer).path
  19. end
  20. 1 def provider_code
  21. 5 matches = ORGANISATION_URL_PATTERN.match(request.referer)
  22. 5 return if matches.nil?
  23. 3 matches[1]
  24. end
  25. 1 private
  26. 1 attr_reader :request, :current_user
  27. end

lib/rack/request_output.rb

0.0% lines covered

11 relevant lines. 0 lines covered and 11 lines missed.
    
  1. module Rack
  2. class RequestOutput
  3. def initialize(app)
  4. @app = app
  5. end
  6. def call(env)
  7. Rails.logger.info { "API HIT => #{env['rack.url_scheme']}://#{env['HTTP_HOST']}#{env['REQUEST_URI']}" }
  8. @app.call(env)
  9. end
  10. end
  11. end